diff --git a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet6_0.verified.txt b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet6_0.verified.txt index 4612db9d9..f52bdd1b7 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet6_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet6_0.verified.txt @@ -1855,6 +1855,70 @@ namespace DynamicData where TDestinationKey : notnull where TSource : notnull where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } public static System.IObservable> TransformSafe(this System.IObservable> source, System.Func transformFactory, System.Action> errorHandler, System.IObservable forceTransform) where TDestination : notnull where TSource : notnull diff --git a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet7_0.verified.txt b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet7_0.verified.txt index 10b8258c4..60608e01b 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet7_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet7_0.verified.txt @@ -1855,6 +1855,70 @@ namespace DynamicData where TDestinationKey : notnull where TSource : notnull where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } public static System.IObservable> TransformSafe(this System.IObservable> source, System.Func transformFactory, System.Action> errorHandler, System.IObservable forceTransform) where TDestination : notnull where TSource : notnull diff --git a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt index 93e985429..63a91b115 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt @@ -1867,6 +1867,70 @@ namespace DynamicData where TDestinationKey : notnull where TSource : notnull where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManyAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func>> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } + public static System.IObservable> TransformManySafeAsync(this System.IObservable> source, System.Func> manySelector, System.Func keySelector, System.Action> errorHandler, System.Collections.Generic.IEqualityComparer? equalityComparer = null, System.Collections.Generic.IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : System.Collections.Specialized.INotifyCollectionChanged, System.Collections.Generic.IEnumerable { } public static System.IObservable> TransformSafe(this System.IObservable> source, System.Func transformFactory, System.Action> errorHandler, System.IObservable forceTransform) where TDestination : notnull where TSource : notnull diff --git a/src/DynamicData.Tests/Cache/TransformManyAsyncFixture.cs b/src/DynamicData.Tests/Cache/TransformManyAsyncFixture.cs new file mode 100644 index 000000000..c8a70dcfa --- /dev/null +++ b/src/DynamicData.Tests/Cache/TransformManyAsyncFixture.cs @@ -0,0 +1,365 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Bogus; +using DynamicData.Kernel; +using DynamicData.Tests.Domain; +using DynamicData.Tests.Utilities; +using FluentAssertions; +using Xunit; + +namespace DynamicData.Tests.Cache; + +public sealed class TransformManyAsyncFixture : IDisposable +{ +#if DEBUG + const int InitialOwnerCount = 7; + const int AddRangeSize = 5; + const int RemoveRangeSize = 3; +#else + const int InitialOwnerCount = 103; + const int AddCount = 53; + const int RemoveCount = 37; +#endif + + const int MinTaskDelay = 10; + const int MaxTaskDelay = 100; + + private readonly ISourceCache _animalOwners = new SourceCache(o => o.Id); + private readonly ChangeSetAggregator _animalOwnerResults; + private readonly Faker _animalOwnerFaker; + private readonly Faker _animalFaker; + private readonly Randomizer _randomizer; + + public TransformManyAsyncFixture() + { + unchecked{ _randomizer = new Randomizer((int)0xf7ee_bee7); } + + _animalFaker = Fakers.Animal.Clone().WithSeed(_randomizer); + _animalOwnerFaker = Fakers.AnimalOwner.Clone().WithSeed(_randomizer).WithInitialAnimals(_animalFaker); + + _animalOwnerResults = _animalOwners.Connect().AsAggregator(); + } + + [Fact] + public void EnumerableResultContainsAllInitialChildrenInSingleChangeSet() + { + // Arrange + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + + // Act + using var animalResults = CreateEnumerableChangeSet().AsAggregator(); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); + animalResults.Messages.Count.Should().Be(1); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Fact] + public void ResultContainsAllInitialChildren() + { + // Arrange + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + + // Act + using var animalResults = CreateObservableCollectionChangeSet().AsAggregator(); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); + animalResults.Messages.Count.Should().Be(1); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Fact] + public void ResultContainsChildrenFromAddedParents() + { + // Arrange + using var animalResults = CreateObservableCollectionChangeSet().AsAggregator(); + + // Act + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); + animalResults.Messages.Count.Should().Be(1); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Fact] + public async Task ResultContainsChildrenFromAddedParentsAsync() + { + // Arrange + var taskTracker = new TaskTracker(FakeDelay); + var shared = CreateObservableCollectionChangeSet(taskTracker.Create).Replay(); + var animalResults = shared.AsAggregator(); + using var connect = shared.Connect(); + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + + // Act + await shared.Take(1); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); + animalResults.Messages.Count.Should().BeGreaterThan(0); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Fact] + public void ResultContainsAddedChildrenFromExistingParents() + { + // Arrange + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + using var animalResults = CreateObservableCollectionChangeSet().AsAggregator(); + + // Act + _animalOwners.Items.ForEach(owner => owner.AddAnimals(_animalFaker, 1, AddCount)); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Fact] + public async Task ResultDoesNotContainChildrenFromRemovedParentsAsync() + { + // Arrange + var taskTracker = new TaskTracker(FakeDelay); + var animalResults = CreateObservableCollectionChangeSet(taskTracker.Create).AsAggregator(); + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + var removedOwners = _randomizer.ListItems(_animalOwners.Items.ToList(), RemoveCount); + _ = taskTracker.Add(() => _animalOwners.Remove(removedOwners)); + + // Act + await taskTracker.WhenAll(); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount - RemoveCount); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Fact] + public void ResultsWorkWithComparer() + { + // Arrange + using var animalResults = CreateObservableCollectionChangeSet(FamilyKey, comparer: Animal.NameComparer).AsAggregator(); + + // Act + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults, FamilyKey, Animal.NameComparer); + } + + [Fact] + public async Task ResultsWithObservableCacheChangesAsync() + { + // Arrange + var taskTracker = new TaskTracker(FakeDelay); + using var animalResults = CreateObservableCacheChangeSet(taskTracker.Create).AsAggregator(); + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + var ownerAddCount = _randomizer.Number(1, AddCount); + taskTracker.Add(() => _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate()), ownerAddCount); + taskTracker.Add(() => _randomizer.ListItem(_animalOwners.Items.ToList()).AddAnimals(_animalFaker, 1, AddCount), AddCount); + + // Act + await taskTracker.WhenAll(); + + // Assert + _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount + ownerAddCount); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ResultCompletesOnlyWhenSourceCompletes(bool completeSource) + { + // Arrange + using var animalResults = CreateObservableCollectionChangeSet().AsAggregator(); + _animalOwners.AddOrUpdate(_animalOwnerFaker.Generate(InitialOwnerCount)); + + // Act + if (completeSource) + { + _animalOwners.Dispose(); + } + + // Assert + _animalOwnerResults.IsCompleted.Should().Be(completeSource); + CheckResultContents(_animalOwners.Items, _animalOwnerResults, animalResults); + } + + [Fact] + public void ResultFailsIfSourceFails() + { + // Arrange + var expectedError = new Exception("Expected"); + var throwObservable = Observable.Throw>(expectedError); + using var results = _animalOwners.Connect().Concat(throwObservable).MergeManyChangeSets(owner => owner.Animals.Connect()).AsAggregator(); + + // Act + _animalOwners.Dispose(); + + // Assert + results.Exception.Should().Be(expectedError); + } + + public void Dispose() + { + _animalOwners.Items.ForEach(owner => owner.Dispose()); + _animalOwnerResults.Dispose(); + _animalOwners.Dispose(); + } + + private AnimalOwner CreateWithSameId(AnimalOwner original) + { + var newOwner = _animalOwnerFaker.Generate(); + var sameId = new AnimalOwner(newOwner.Name, original.Id); + sameId.Animals.AddRange(newOwner.Animals.Items); + return sameId; + } + + private static void CheckResultContents(IEnumerable owners, ChangeSetAggregator ownerResults, ChangeSetAggregator animalResults, Func keySelector, IComparer comparer) + where T : notnull + { + var expectedOwners = owners.ToList(); + + // These should be subsets of each other + expectedOwners.Should().BeSubsetOf(ownerResults.Data.Items); + ownerResults.Data.Items.Count().Should().Be(expectedOwners.Count); + + var allAnimals = expectedOwners.SelectMany(owner => owner.Animals.Items).ToList(); + var expectedAnimals = allAnimals.GroupBy(keySelector).Select(group => group.OrderBy(a => a, comparer).First()).ToList(); + + expectedAnimals.Should().BeSubsetOf(animalResults.Data.Items); + animalResults.Data.Count.Should().Be(expectedAnimals.Count); + } + + private static void CheckResultContents(IEnumerable owners, ChangeSetAggregator ownerResults, ChangeSetAggregator animalResults) + where T : notnull + { + var expectedOwners = owners.ToList(); + + // These should be subsets of each other + expectedOwners.Should().BeSubsetOf(ownerResults.Data.Items); + ownerResults.Data.Items.Count().Should().Be(expectedOwners.Count); + + // All owner animals should be in the results + foreach (var owner in owners) + { + owner.Animals.Items.Should().BeSubsetOf(animalResults.Data.Items); + } + + // Results should not have more than the total number of animals + animalResults.Data.Count.Should().Be(owners.Sum(owner => owner.Animals.Count)); + } + + private Func RandomDelay => () => Task.Delay(_randomizer.Number(MinTaskDelay, MaxTaskDelay)); + + private static Func FakeDelay => () => Task.CompletedTask; + + private static int IdKey(Animal a) => a.Id; + private static AnimalFamily FamilyKey(Animal a) => a.Family; + + private static Func>> SelectObservableCollection(Func? delayFactory = null) => + CreateSelector(static owner => owner.ObservableCollection, delayFactory); + + private static Func>> SelectObservableCache(Func? delayFactory = null) => + CreateSelector(static owner => owner.ObservableCache, delayFactory); + + private static Func>> SelectEnumerable(Func? delayFactory = null) => + CreateSelector(static owner => owner.Animals.Items, delayFactory); + + private static Func> CreateSelector(Func selector, Func? delayFactory = null) => + (delayFactory != null) + // If a delay factory is given, make it async + ? (async (owner, guid) => + { + await delayFactory().ConfigureAwait(false); + return selector(owner); + }) + + // Otherwise make it not async + : (owner, guid) => Task.FromResult(selector(owner)); + + private IObservable> CreateObservableCollectionChangeSet(Func? delayFactory = null, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) => + CreateObservableCollectionChangeSet(IdKey, delayFactory, equalityComparer, comparer); + + private IObservable> CreateObservableCollectionChangeSet(Func keySelector, Func? delayFactory = null, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TKey : notnull + => _animalOwners.Connect().TransformManyAsync(SelectObservableCollection(delayFactory), keySelector, equalityComparer, comparer); + + private IObservable> CreateEnumerableChangeSet(Func? delayFactory = null, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) => + CreateEnumerableChangeSet(IdKey, delayFactory, equalityComparer, comparer); + + private IObservable> CreateEnumerableChangeSet(Func keySelector, Func? delayFactory = null, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TKey : notnull + => _animalOwners.Connect().TransformManyAsync(SelectEnumerable(delayFactory), keySelector, equalityComparer, comparer); + + private IObservable> CreateObservableCacheChangeSet(Func? delayFactory = null) + => _animalOwners.Connect().TransformManyAsync(SelectObservableCache(delayFactory)); + + private class TaskTracker(Func delayFactory) + { + private readonly object _lock = new(); + private readonly List _tasks = []; + + public Task Create() => Add(delayFactory()); + + public Task Add(Task task) => task.With(t => { lock (_lock) _tasks.Add(task); } ); + + public IEnumerable Add(IEnumerable tasks) => tasks.With(ts => ts.ForEach(t => Add(t))); + + public void Add(Action action, int count) => Add(Task.WhenAll(Enumerable.Range(0, count).Select(_ => FromAction(action)))); + + public Task Add(Action action) => Add(FromAction(action)); + + public Task Add(Func func) + { + var task = Task.Run(async () => + { + await delayFactory(); + return func(); + }); + + Add(task); + return task; + } + + public async Task WhenAll() + { + // Wait on all tasks until no more are being added + var list = GetList(); + while (list.Count > 0) + { + await Task.WhenAll(list); + list = GetList(); + } + + // Wait a little extra + await delayFactory(); + } + + private Task FromAction(Action action) => + Task.Run(async () => + { + await delayFactory(); + action(); + }); + + private List GetList() + { + lock(_lock) + { + var result = _tasks.ToList(); + _tasks.Clear(); + return result; + } + } + } +} diff --git a/src/DynamicData.Tests/Domain/Animal.cs b/src/DynamicData.Tests/Domain/Animal.cs index b97f6a6ee..744b058ee 100644 --- a/src/DynamicData.Tests/Domain/Animal.cs +++ b/src/DynamicData.Tests/Domain/Animal.cs @@ -44,18 +44,45 @@ public bool IncludeInResults public override string ToString() => $"{FormalName} ({Family}) [{Id:x4}]"; public override int GetHashCode() => HashCode.Combine(Id, Name, Family, Type); -} -public sealed class AnimalEqualityComparer : IEqualityComparer -{ - public static AnimalEqualityComparer Instance { get; } = new(); + public static IComparer NameComparer { get; } = new AnimalAlphabeticComparer(); + + public static IEqualityComparer NameTypeCompare { get; } = new AnimalEqualityComparer(); + + public static IEqualityComparer IdCompare { get; } = new AnimalIdComparer(); + + private sealed class AnimalAlphabeticComparer : IComparer + { + public int Compare([DisallowNull] Animal x, [DisallowNull] Animal y) => (x, y) switch + { + (null, null) => 0, + (Animal a, Animal b) => string.Compare(a.FormalName, b.FormalName, StringComparison.OrdinalIgnoreCase), + (null, _) => 1, + _ => -1 + }; + } - public bool Equals(Animal? x, Animal? y) => (x, y) switch + private sealed class AnimalIdComparer : IEqualityComparer { - (null, null) => true, - (Animal a, Animal b) => (a.Type == b.Type) && (a.Family == b.Family) && (a.Name == b.Name), - _ => false, - }; + public bool Equals([DisallowNull] Animal x, [DisallowNull] Animal y) => (x, y) switch + { + (null, null) => true, + (Animal a, Animal b) => a.Id == b.Id, + _ => false, + }; + + public int GetHashCode([DisallowNull] Animal obj) => HashCode.Combine(obj?.Id ?? 0); + } - public int GetHashCode([DisallowNull] Animal obj) => HashCode.Combine(obj?.Name ?? string.Empty, obj.Type, obj.Family); + private sealed class AnimalEqualityComparer : IEqualityComparer + { + public bool Equals(Animal? x, Animal? y) => (x, y) switch + { + (null, null) => true, + (Animal a, Animal b) => (a.Type == b.Type) && (a.Family == b.Family) && (a.Name == b.Name), + _ => false, + }; + + public int GetHashCode([DisallowNull] Animal obj) => HashCode.Combine(obj?.Name ?? string.Empty, obj.Type, obj.Family); + } } diff --git a/src/DynamicData.Tests/Domain/AnimalOwner.cs b/src/DynamicData.Tests/Domain/AnimalOwner.cs index 08925128c..26049705b 100644 --- a/src/DynamicData.Tests/Domain/AnimalOwner.cs +++ b/src/DynamicData.Tests/Domain/AnimalOwner.cs @@ -1,4 +1,7 @@ using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Reactive.Disposables; using DynamicData.Binding; namespace DynamicData.Tests.Domain; @@ -6,6 +9,9 @@ namespace DynamicData.Tests.Domain; internal sealed class AnimalOwner(string name, Guid? id = null, bool include = true) : AbstractNotifyPropertyChanged, IDisposable { private bool _includeInResults = include; + private readonly SerialDisposable _collectionSubscription = new(); + private ReadOnlyObservableCollection? _collection; + private IObservableCache? _observableCache; public Guid Id { get; } = id ?? Guid.NewGuid(); @@ -13,13 +19,28 @@ internal sealed class AnimalOwner(string name, Guid? id = null, bool include = t public ISourceList Animals { get; } = new SourceList(); + public ReadOnlyObservableCollection ObservableCollection => _collection ??= CreateObservableCollection(); + + public IObservableCache ObservableCache => _observableCache ??= Animals.Connect().AddKey(a => a.Id).AsObservableCache(); + public bool IncludeInResults { get => _includeInResults; set => SetAndRaise(ref _includeInResults, value); } - public void Dispose() => Animals.Dispose(); + public void Dispose() + { + _collectionSubscription.Dispose(); + _observableCache?.Dispose(); + Animals.Dispose(); + } public override string ToString() => $"{Name} [{Animals.Count} Animals] ({Id:B})"; + + private ReadOnlyObservableCollection CreateObservableCollection() + { + _collectionSubscription.Disposable = Animals.Connect().Bind(out var collection).Subscribe(); + return collection; + } } diff --git a/src/DynamicData.Tests/Domain/Fakers.cs b/src/DynamicData.Tests/Domain/Fakers.cs index 972d3df94..7bf51eeec 100644 --- a/src/DynamicData.Tests/Domain/Fakers.cs +++ b/src/DynamicData.Tests/Domain/Fakers.cs @@ -1,4 +1,5 @@ using Bogus; +using DynamicData.Tests.Utilities; namespace DynamicData.Tests.Domain; @@ -47,13 +48,19 @@ internal static class Fakers public static Faker Market { get; } = new Faker().CustomInstantiator(faker => new Market($"{faker.Commerce.ProductName()} Id#{faker.Random.AlphaNumeric(5)}")); public static Faker WithInitialAnimals(this Faker existing, Faker animalFaker, int minCount, int maxCount) => - existing.FinishWith((faker, owner) => owner.Animals.AddRange(animalFaker.GenerateLazy(faker.Random.Number(minCount, maxCount)))); + existing.FinishWith((faker, owner) => owner.Animals.AddRange(animalFaker.GenerateBetween(minCount, maxCount))); public static Faker WithInitialAnimals(this Faker existing, Faker animalFaker, int maxCount) => WithInitialAnimals(existing, animalFaker, 0, maxCount); public static Faker WithInitialAnimals(this Faker existing, Faker animalFaker) => WithInitialAnimals(existing, animalFaker, MinAnimals, MaxAnimals); + + public static AnimalOwner AddAnimals(this AnimalOwner owner, Faker animalFaker, int minCount, int maxCount) => + owner.With(o => o.Animals.AddRange(animalFaker.GenerateBetween(minCount, maxCount))); + + public static AnimalOwner AddAnimals(this AnimalOwner owner, Faker animalFaker, int count) => + owner.With(o => o.Animals.AddRange(animalFaker.Generate(count))); } internal static class FakerExtensions diff --git a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs index c720bbebb..c21606c9a 100644 --- a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs +++ b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs @@ -12,6 +12,9 @@ internal sealed class ChangeSetMergeTracker(Func _resultCache = new(); + private bool _hasCompleted; + + public void MarkComplete() => _hasCompleted = true; public void RemoveItems(IEnumerable> items, IObserver>? observer = null) { @@ -106,6 +109,11 @@ public void EmitChanges(IObserver> observer) { observer.OnNext(changeSet); } + + if (_hasCompleted) + { + observer.OnCompleted(); + } } private void OnItemAdded(TObject item, TKey key) diff --git a/src/DynamicData/Cache/Internal/TransformManyAsync.cs b/src/DynamicData/Cache/Internal/TransformManyAsync.cs new file mode 100644 index 000000000..e45adebed --- /dev/null +++ b/src/DynamicData/Cache/Internal/TransformManyAsync.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2011-2023 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.Reactive.Disposables; +using System.Reactive.Linq; + +using DynamicData.Internal; +using DynamicData.Kernel; + +namespace DynamicData.Cache.Internal; + +internal sealed class TransformManyAsync(IObservable> source, Func>>> selector, IEqualityComparer? equalityComparer, IComparer? comparer, Action>? errorHandler = null) + where TSource : notnull + where TKey : notnull + where TDestination : notnull + where TDestinationKey : notnull +{ + public IObservable> Run() => Observable.Create>( + observer => + { + var locker = new object(); + var cache = new Cache, TKey>(); + var updateCounter = 0; + + // Transformation Function: + // Create the Child Observable by invoking the async selector, appending the counter and the synchronize + // Pass the result to a new ChangeSetCache instance. + ChangeSetCache Transform_(TSource obj, TKey key) => new( + Observable.Defer(() => selector(obj, key)) + .Do(_ => Interlocked.Increment(ref updateCounter)) + .Synchronize(locker!)); + + // This is manages all of the changes + var changeTracker = new ChangeSetMergeTracker(() => cache.Items, comparer, equalityComparer); + + // Transform to a cache changeset of child caches, synchronize, clone changes to the local copy, and publish. + // Increment updateCounter BEFORE the lock so that incoming changesets will cause the downstream changeset to be delayed + // until all pending changesets have been handled. + var shared = + (errorHandler is null ? source.Transform(Transform_) : source.TransformSafe(Transform_, errorHandler)) + .Do(_ => Interlocked.Increment(ref updateCounter)) + .Synchronize(locker) + .Do(cache.Clone) + .Publish(); + + // Merge the child changeset changes together and apply to the tracker + // Emit the changeset if there are no other pending changes + var subMergeMany = shared + .MergeMany(cacheChangeSet => cacheChangeSet.Source) + .SubscribeSafe( + changes => changeTracker.ProcessChangeSet(changes, Interlocked.Decrement(ref updateCounter) == 0 ? observer : null), + observer.OnError); + + // When a source item is removed, all of its sub-items need to be removed + // Emit the changeset if there are no other pending changes + var subRemove = shared + .OnItemRemoved(changeSetCache => changeTracker.RemoveItems(changeSetCache.Cache.KeyValues), invokeOnUnsubscribe: false) + .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.Cache.KeyValues)) + .SubscribeSafe( + _ => + { + if (Interlocked.Decrement(ref updateCounter) == 0) + { + changeTracker.EmitChanges(observer); + } + }, + observer.OnError, + () => + { + if (Volatile.Read(ref updateCounter) == 0) + { + observer.OnCompleted(); + } + else + { + changeTracker.MarkComplete(); + } + }); + + return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); + }); +} diff --git a/src/DynamicData/Cache/ObservableCacheEx.cs b/src/DynamicData/Cache/ObservableCacheEx.cs index 726dc1743..9244fe607 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; @@ -11,6 +12,7 @@ using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Runtime.CompilerServices; using System.Transactions; using DynamicData.Binding; using DynamicData.Cache; @@ -4913,6 +4915,313 @@ public static IObservable> TransformMa where TSource : notnull where TSourceKey : notnull => new TransformMany(source, manySelector, keySelector).Run(); + /// + /// Extension method similar to except that it allows the tranformation function to be an async method. Also supports comparison and sorting to prioritize values the same destination key returned from multiple sources. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable changeset with the transformed values. + /// The source. + /// Async function to transform a and into an of . + /// The key selector which must be unique across all. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManyAsync(this IObservable> source, Func>> manySelector, Func keySelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + manySelector.ThrowArgumentNullExceptionIfNull(nameof(manySelector)); + + return new TransformManyAsync(source, CreateChangeSetTranformer(manySelector, keySelector), equalityComparer, comparer).Run(); + } + + /// + /// Extension method similar to except that it allows the tranformation function to be an async method. Also supports comparison and sorting to prioritize values the same destination key returned from multiple sources. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable changeset with the transformed values. + /// The source. + /// Async function to transform a into an of . + /// The key selector which must be unique across all. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManyAsync(this IObservable> source, Func>> manySelector, Func keySelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull => source.TransformManyAsync((val, _) => manySelector(val), keySelector, equalityComparer, comparer); + + /// + /// Extension method similar to except that it allows the tranformation function to be an async method. Also supports comparison and sorting to prioritize values the same destination key returned from multiple sources. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// The type of an observable collection of . + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a and into an of . + /// The key selector which must be unique across all. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManyAsync(this IObservable> source, Func> manySelector, Func keySelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : INotifyCollectionChanged, IEnumerable + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + manySelector.ThrowArgumentNullExceptionIfNull(nameof(manySelector)); + + return new TransformManyAsync(source, CreateChangeSetTranformer(manySelector, keySelector), equalityComparer, comparer).Run(); + } + + /// + /// Extension method similar to except that it allows the tranformation function to be an async method. Also supports comparison and sorting to prioritize values the same destination key returned from multiple sources. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// The type of an observable collection of . + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a into an of . + /// The key selector which must be unique across all. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManyAsync(this IObservable> source, Func> manySelector, Func keySelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : INotifyCollectionChanged, IEnumerable => source.TransformManyAsync((val, _) => manySelector(val), keySelector, equalityComparer, comparer); + + /// + /// Extension method similar to except that it allows the tranformation function to be an async method. Also supports comparison and sorting to prioritize values the same destination key returned from multiple sources. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a and into an of . + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManyAsync(this IObservable> source, Func>> manySelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + manySelector.ThrowArgumentNullExceptionIfNull(nameof(manySelector)); + + return new TransformManyAsync(source, CreateChangeSetTranformer(manySelector), equalityComparer, comparer).Run(); + } + + /// + /// Extension method similar to except that it allows the tranformation function to be an async method. Also supports comparison and sorting to prioritize values the same destination key returned from multiple sources. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a and into an of . + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManyAsync(this IObservable> source, Func>> manySelector, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull => source.TransformManyAsync((val, _) => manySelector(val), equalityComparer, comparer); + + /// + /// Extension method similar to except it accepts an error handler so that failed transformations are not fatal errors. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable changeset with the transformed values. + /// The source. + /// Async function to transform a and into an of . + /// The key selector which must be unique across all. + /// Callback function for handling an errors. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManySafeAsync(this IObservable> source, Func>> manySelector, Func keySelector, Action> errorHandler, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + manySelector.ThrowArgumentNullExceptionIfNull(nameof(manySelector)); + errorHandler.ThrowArgumentNullExceptionIfNull(nameof(errorHandler)); + + return new TransformManyAsync(source, CreateChangeSetTranformer(manySelector, keySelector), equalityComparer, comparer, errorHandler).Run(); + } + + /// + /// Extension method similar to except it accepts an error handler so that failed transformations are not fatal errors. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable changeset with the transformed values. + /// The source. + /// Async function to transform a into an of . + /// The key selector which must be unique across all. + /// Callback function for handling an errors. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManySafeAsync(this IObservable> source, Func>> manySelector, Func keySelector, Action> errorHandler, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull => source.TransformManySafeAsync((val, _) => manySelector(val), keySelector, errorHandler, equalityComparer, comparer); + + /// + /// Extension method similar to except it accepts an error handler so that failed transformations are not fatal errors. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// The type of an observable collection of . + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a and into an of . + /// The key selector which must be unique across all. + /// Callback function for handling an errors. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManySafeAsync(this IObservable> source, Func> manySelector, Func keySelector, Action> errorHandler, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : INotifyCollectionChanged, IEnumerable + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + manySelector.ThrowArgumentNullExceptionIfNull(nameof(manySelector)); + errorHandler.ThrowArgumentNullExceptionIfNull(nameof(errorHandler)); + + return new TransformManyAsync(source, CreateChangeSetTranformer(manySelector, keySelector), equalityComparer, comparer, errorHandler).Run(); + } + + /// + /// Extension method similar to except it accepts an error handler so that failed transformations are not fatal errors. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// The type of an observable collection of . + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a into an of . + /// The key selector which must be unique across all. + /// Callback function for handling an errors. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManySafeAsync(this IObservable> source, Func> manySelector, Func keySelector, Action> errorHandler, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : INotifyCollectionChanged, IEnumerable => source.TransformManySafeAsync((val, _) => manySelector(val), keySelector, errorHandler, equalityComparer, comparer); + + /// + /// Extension method similar to except it accepts an error handler so that failed transformations are not fatal errors. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a and into an of . + /// Callback function for handling an errors. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManySafeAsync(this IObservable> source, Func>> manySelector, Action> errorHandler, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + { + source.ThrowArgumentNullExceptionIfNull(nameof(source)); + manySelector.ThrowArgumentNullExceptionIfNull(nameof(manySelector)); + errorHandler.ThrowArgumentNullExceptionIfNull(nameof(errorHandler)); + + return new TransformManyAsync(source, CreateChangeSetTranformer(manySelector), equalityComparer, comparer, errorHandler).Run(); + } + + /// + /// Extension method similar to except it accepts an error handler so that failed transformations are not fatal errors. + /// + /// The type of the destination. + /// The type of the destination key. + /// The type of the source. + /// The type of the source key. + /// An observable with the transformed change set. + /// The source. + /// Async function to transform a into an of . + /// Callback function for handling an errors. + /// Optional instance to determine if two elements are the same. + /// Optional instance to determine which element to emit if the same key is emitted from multiple child changesets. + /// Because the transformations are asynchronous, unlike TransformMany, each sub-collection could be emitted via a separate changeset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SuppressMessage("Roslynator", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "By Design.")] + public static IObservable> TransformManySafeAsync(this IObservable> source, Func>> manySelector, Action> errorHandler, IEqualityComparer? equalityComparer = null, IComparer? comparer = null) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull => source.TransformManySafeAsync((val, _) => manySelector(val), errorHandler, equalityComparer, comparer); + /// /// Projects each update item to a new form using the specified transform function, /// providing an error handling action to safely handle transform errors without killing the stream. @@ -5920,4 +6229,23 @@ private static IObservable TrueFor(this IObservable where TObject : notnull where TKey : notnull where TValue : notnull => new TrueFor(source, observableSelector, collectionMatcher).Run(); + + private static Func>>> CreateChangeSetTranformer(Func>> manySelector, Func keySelector) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull => async (val, key) => (await manySelector(val, key).ConfigureAwait(false)).AsObservableChangeSet(keySelector); + + private static Func>>> CreateChangeSetTranformer(Func> manySelector, Func keySelector) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull + where TCollection : INotifyCollectionChanged, IEnumerable => async (val, key) => (await manySelector(val, key).ConfigureAwait(false)).ToObservableChangeSet().AddKey(keySelector); + + private static Func>>> CreateChangeSetTranformer(Func>> manySelector) + where TDestination : notnull + where TDestinationKey : notnull + where TSource : notnull + where TSourceKey : notnull => async (val, key) => (await manySelector(val, key).ConfigureAwait(false)).Connect(); } diff --git a/src/DynamicData/Internal/ObservableEx.cs b/src/DynamicData/Internal/ObservableEx.cs index 0d4555b8f..c390d01d9 100644 --- a/src/DynamicData/Internal/ObservableEx.cs +++ b/src/DynamicData/Internal/ObservableEx.cs @@ -10,4 +10,13 @@ internal static class ObservableEx { public static IDisposable SubscribeSafe(this IObservable observable, Action onNext, Action onError, Action onComplete) => observable.SubscribeSafe(Observer.Create(onNext, onError, onComplete)); + + public static IDisposable SubscribeSafe(this IObservable observable, Action onNext, Action onError) => + observable.SubscribeSafe(Observer.Create(onNext, onError)); + + public static IDisposable SubscribeSafe(this IObservable observable, Action onNext, Action onComplete) => + observable.SubscribeSafe(Observer.Create(onNext, onComplete)); + + public static IDisposable SubscribeSafe(this IObservable observable, Action onNext) => + observable.SubscribeSafe(Observer.Create(onNext)); }