Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: EditDiff extension method for IObservable<IEnumerable<T>> #738

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions src/DynamicData.Tests/Cache/EditDiffChangeSetFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using FluentAssertions;

using Xunit;

namespace DynamicData.Tests.Cache;

public class EditDiffChangeSetFixture
{
private const int MaxItems = 1097;

[Fact]
public void NullChecksArePerformed()
{
Assert.Throws<ArgumentNullException>(() => Observable.Empty<IEnumerable<Person>>().EditDiff<Person, int>(null!));
Assert.Throws<ArgumentNullException>(() => default(IObservable<IEnumerable<Person>>)!.EditDiff<Person, int>(null!));
}

[Fact]
public void ItemsFromEnumerableAreAddedToChangeSet()
{
// having
var enumerable = CreatePeople(0, MaxItems, "Name");
var enumObservable = Observable.Return(enumerable);

// when
var observableChangeSet = enumObservable.EditDiff(p => p.Id);
using var results = observableChangeSet.AsAggregator();

// then
results.Data.Count.Should().Be(MaxItems);
results.Messages.Count.Should().Be(1);
}

[Fact]
public void ItemsRemovedFromEnumerableAreRemovedFromChangeSet()
{
// having
var enumerable = CreatePeople(0, MaxItems, "Name");
var enumObservable = new[] {enumerable, Enumerable.Empty<Person>()}.ToObservable();

// when
var observableChangeSet = enumObservable.EditDiff(p => p.Id);
using var results = observableChangeSet.AsAggregator();

// then
results.Data.Count.Should().Be(0);
results.Messages.Count.Should().Be(2);
results.Messages[0].Adds.Should().Be(MaxItems);
results.Messages[1].Removes.Should().Be(MaxItems);
results.Messages[1].Updates.Should().Be(0);
}

[Fact]
public void ItemsUpdatedAreUpdatedInChangeSet()
{
// having
var enumerable1 = CreatePeople(0, MaxItems * 2, "Name");
var enumerable2 = CreatePeople(MaxItems, MaxItems, "Update");
var enumObservable = new[] { enumerable1, enumerable2 }.ToObservable();

// when
var observableChangeSet = enumObservable.EditDiff(p => p.Id);
using var results = observableChangeSet.AsAggregator();

// then
results.Data.Count.Should().Be(MaxItems);
results.Messages.Count.Should().Be(2);
results.Messages[0].Adds.Should().Be(MaxItems * 2);
results.Messages[1].Updates.Should().Be(MaxItems);
results.Messages[1].Removes.Should().Be(MaxItems);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void ResultCompletesIfAndOnlyIfSourceCompletes(bool completeSource)
{
// having
var enumerable = CreatePeople(0, MaxItems, "Name");
var enumObservable = Observable.Return(enumerable);
if (!completeSource)
{
enumObservable = enumObservable.Concat(Observable.Never<IEnumerable<Person>>());
}
bool completed = false;

// when
using var results = enumObservable.Subscribe(_ => { }, () => completed = true);

// then
completed.Should().Be(completeSource);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void ResultFailsIfAndOnlyIfSourceFails (bool failSource)
{
// having
var enumerable = CreatePeople(0, MaxItems, "Name");
var enumObservable = Observable.Return(enumerable);
var testException = new Exception("Test");
if (failSource)
{
enumObservable = enumObservable.Concat(Observable.Throw<IEnumerable<Person>>(testException));
}
var receivedError = default(Exception);

// when
using var results = enumObservable.Subscribe(_ => { }, err => receivedError = err);

// then
receivedError.Should().Be(failSource ? testException : default);
}

[Trait("Performance", "Manual run only")]
[Theory]
[InlineData(7, 3, 5)]
[InlineData(233, 113, MaxItems)]
[InlineData(233, 0, MaxItems)]
[InlineData(233, 233, MaxItems)]
[InlineData(2521, 1187, MaxItems)]
[InlineData(2521, 0, MaxItems)]
[InlineData(2521, 2521, MaxItems)]
[InlineData(5081, 2683, MaxItems)]
[InlineData(5081, 0, MaxItems)]
[InlineData(5081, 5081, MaxItems)]
public void Perf(int collectionSize, int updateSize, int maxItems)
{
Debug.Assert(updateSize <= collectionSize);

// having
var enumerables = Enumerable.Range(1, maxItems - 1)
.Select(n => n * (collectionSize - updateSize))
.Select(index => CreatePeople(index, updateSize, "Overlap")
.Concat(CreatePeople(index + updateSize, collectionSize - updateSize, "Name")))
.Prepend(CreatePeople(0, collectionSize, "Name"));
var enumObservable = enumerables.ToObservable();

// when
var observableChangeSet = enumObservable.EditDiff(p => p.Id);
using var results = observableChangeSet.AsAggregator();

// then
results.Data.Count.Should().Be(collectionSize);
results.Messages.Count.Should().Be(maxItems);
results.Summary.Overall.Adds.Should().Be((collectionSize * maxItems) - (updateSize * (maxItems - 1)));
results.Summary.Overall.Removes.Should().Be((collectionSize - updateSize) * (maxItems - 1));
results.Summary.Overall.Updates.Should().Be(updateSize * (maxItems - 1));
}

private static Person CreatePerson(int id, string name) => new(id, name);

private static IEnumerable<Person> CreatePeople(int baseId, int count, string baseName) =>
Enumerable.Range(baseId, count).Select(i => CreatePerson(i, baseName + i));

private class Person
{
public Person(int id, string name)
{
Id = id;
Name = name;
}

public int Id { get; }

public string Name { get; }
}
}
32 changes: 32 additions & 0 deletions src/DynamicData/Cache/Internal/EditDiffChangeSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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.Kernel;

namespace DynamicData.Cache.Internal;

internal sealed class EditDiffChangeSet<TObject, TKey>
where TObject : notnull
where TKey : notnull
{
private readonly IObservable<IEnumerable<TObject>> _source;

private readonly IEqualityComparer<TObject> _equalityComparer;

private readonly Func<TObject, TKey> _keySelector;

public EditDiffChangeSet(IObservable<IEnumerable<TObject>> source, Func<TObject, TKey> keySelector, IEqualityComparer<TObject>? equalityComparer)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
_keySelector = keySelector ?? throw new ArgumentNullException(nameof(keySelector));
_equalityComparer = equalityComparer ?? EqualityComparer<TObject>.Default;
}

public IObservable<IChangeSet<TObject, TKey>> Run() =>
ObservableChangeSet.Create(
cache => _source.Subscribe(items => cache.EditDiff(items, _equalityComparer), () => cache.Dispose()),
_keySelector);
}
27 changes: 27 additions & 0 deletions src/DynamicData/Cache/ObservableCacheEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,33 @@ public static void EditDiff<TObject, TKey>(this ISourceCache<TObject, TKey> sour
editDiff.Edit(allItems);
}

/// <summary>
/// Converts an Observable of Enumerable to an Observable ChangeSet that updates when the enumerables changes. Counterpart operator to <see cref="ToCollection{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}})"/>.
/// </summary>
/// <typeparam name="TObject">The type of the object.</typeparam>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <param name="source">The source.</param>
/// <param name="keySelector">Key Selection Function for the ChangeSet.</param>
/// <param name="equalityComparer">Optional <see cref="IEqualityComparer{T}"/> instance to use for comparing values.</param>
/// <returns>An observable cache.</returns>
/// <exception cref="System.ArgumentNullException">source.</exception>
public static IObservable<IChangeSet<TObject, TKey>> EditDiff<TObject, TKey>(this IObservable<IEnumerable<TObject>> source, Func<TObject, TKey> keySelector, IEqualityComparer<TObject>? equalityComparer = null)
where TObject : notnull
where TKey : notnull
{
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}

if (keySelector is null)
{
throw new ArgumentNullException(nameof(keySelector));
}

return new EditDiffChangeSet<TObject, TKey>(source, keySelector, equalityComparer).Run();
}

/// <summary>
/// Signal observers to re-evaluate the specified item.
/// </summary>
Expand Down