diff --git a/src/DynamicData.Tests/Cache/InnerJoinFixture.cs b/src/DynamicData.Tests/Cache/InnerJoinFixture.cs index dcbd37c6..bd5ffc05 100644 --- a/src/DynamicData.Tests/Cache/InnerJoinFixture.cs +++ b/src/DynamicData.Tests/Cache/InnerJoinFixture.cs @@ -103,6 +103,59 @@ public void Dispose() _result?.Dispose(); } + [Fact] + public void RefreshRightKey() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); + }); + + var refreshItem = _right.Lookup(2).Value; + + + // Change pairing + refreshItem.Name = "Device3"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Keys.Should().Contain(("Device3", 2)); + + + // Remove pairing + refreshItem.Name = "Device4"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(2); + _result.Data.Keys.Should().NotContain(pair => pair.rightKey == 2); + + + // Restore pairing + refreshItem.Name = "Device2"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Keys.Should().Contain(("Device2", 2)); + + + // No change + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Keys.Should().Contain(("Device2", 2)); + } + [Fact] public void RemoveVarious() { @@ -159,6 +212,53 @@ public void UpdateRight() _result.Data.Count.Should().Be(3); } + [Fact] + public void UpdateRightKey() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); + }); + + + // Change pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device3")); + + _result.Data.Count.Should().Be(3); + _result.Data.Keys.Should().Contain(("Device3", 2)); + + + // Remove pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device4")); + + _result.Data.Count.Should().Be(2); + _result.Data.Keys.Should().NotContain(pair => pair.rightKey == 2); + + + // Restore pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device2")); + + _result.Data.Count.Should().Be(3); + _result.Data.Keys.Should().Contain(("Device2", 2)); + + + // No change + _right.AddOrUpdate(new DeviceMetaData(2,"Device2")); + + _result.Data.Count.Should().Be(3); + _result.Data.Keys.Should().Contain(("Device2", 2)); + } [Fact] public void MultipleRight() @@ -284,7 +384,7 @@ public class DeviceMetaData(int key, string name, bool isAutoConnect = false) : { public bool IsAutoConnect { get; } = isAutoConnect; public int Key { get; } = key; - public string Name { get; } = name; + public string Name { get; set; } = name; public override string ToString() => $"Key: {Key}. Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; diff --git a/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs b/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs index 77f18ff2..edaad944 100644 --- a/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs +++ b/src/DynamicData.Tests/Cache/InnerJoinManyFixture.cs @@ -74,6 +74,52 @@ public void Dispose() _result.Dispose(); } + [Fact] + public void RefreshRightKey() + { + _people.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Person() { Name = "Person #1", ParentName = string.Empty } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #3", ParentName = "Person #2" } ); + }); + + var refreshPerson = _people.Lookup("Person #2").Value; + + + // Change pairing + refreshPerson.ParentName = "Person #3"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(2); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().NotContain(("Person #1", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #3", "Person #2")); + + + // Remove pairing + refreshPerson.ParentName = "Person #4"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(1); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (parentName: family.Parent.Name, chilldName: child.Name))).Should().NotContain(pair => pair.chilldName == "Person #2"); + + + // Restore pairing + refreshPerson.ParentName = "Person #1"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(2); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + + + // No change + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(2); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + } + [Fact] public void RemoveChild() { @@ -136,6 +182,47 @@ public void UpdateParent() AssertDataIsCorrectlyFormed(updatedPeople); } + [Fact] + public void UpdateRightKey() + { + _people.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Person() { Name = "Person #1", ParentName = string.Empty } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #3", ParentName = "Person #2" } ); + }); + + + // Change pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #3" }); + + _result.Data.Count.Should().Be(2); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().NotContain(("Person #1", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #3", "Person #2")); + + + // Remove pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #4" }); + + _result.Data.Count.Should().Be(1); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (parentName: family.Parent.Name, chilldName: child.Name))).Should().NotContain(pair => pair.chilldName == "Person #2"); + + + // Restore pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" }); + + _result.Data.Count.Should().Be(2); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + + + // No change + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" }); + + _result.Data.Count.Should().Be(2); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + } + private void AssertDataIsCorrectlyFormed(Person[] allPeople, params string[] missingParents) { var grouped = allPeople.GroupBy(p => p.ParentName).Where(p => p.Any() && !missingParents.Contains(p.Key)).AsArray(); diff --git a/src/DynamicData.Tests/Cache/LeftJoinFixture.cs b/src/DynamicData.Tests/Cache/LeftJoinFixture.cs index e290d966..c5d0fcd3 100644 --- a/src/DynamicData.Tests/Cache/LeftJoinFixture.cs +++ b/src/DynamicData.Tests/Cache/LeftJoinFixture.cs @@ -16,12 +16,12 @@ public class LeftJoinFixture : IDisposable private readonly ChangeSetAggregator _result; - private readonly SourceCache _right; + private readonly SourceCache _right; public LeftJoinFixture() { _left = new SourceCache(device => device.Name); - _right = new SourceCache(device => device.Name); + _right = new SourceCache(device => device.Key); _result = _left.Connect().LeftJoin(_right.Connect(), meta => meta.Name, (key, device, meta) => new DeviceWithMetadata(device, meta)).AsAggregator(); } @@ -59,9 +59,9 @@ public void AddLetThenRight() _right.Edit( innerCache => { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); }); _result.Data.Count.Should().Be(3); @@ -75,9 +75,9 @@ public void AddRightOnly() _right.Edit( innerCache => { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); }); _result.Data.Count.Should().Be(0); @@ -89,9 +89,9 @@ public void AddRightThenLeft() _right.Edit( innerCache => { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); }); _left.Edit( @@ -114,6 +114,59 @@ public void Dispose() _result.Dispose(); } + [Fact] + public void RefreshRightKey() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + }); + + var refreshItem = _right.Lookup(2).Value; + + + // Change pairing + refreshItem.Name = "Device3"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().NotContain(("Device2", 2)); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().Contain(("Device3", 2)); + + + // Remove pairing + refreshItem.Name = "Device4"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().NotContain(pair => pair.Key == 2); + + + // Restore pairing + refreshItem.Name = "Device2"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().Contain(("Device2", 2)); + + + // No change + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().Contain(("Device2", 2)); + } + [Fact] public void RemoveVarious() { @@ -128,12 +181,12 @@ public void RemoveVarious() _right.Edit( innerCache => { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); }); - _right.Remove("Device3"); + _right.Remove(3); _result.Data.Count.Should().Be(3); _result.Data.Items.Count(dwm => dwm.MetaData != Optional.None).Should().Be(2); @@ -148,9 +201,9 @@ public void UpdateRight() _right.Edit( innerCache => { - innerCache.AddOrUpdate(new DeviceMetaData("Device1")); - innerCache.AddOrUpdate(new DeviceMetaData("Device2")); - innerCache.AddOrUpdate(new DeviceMetaData("Device3")); + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); }); _left.Edit( @@ -166,6 +219,55 @@ public void UpdateRight() _result.Data.Items.All(dwm => dwm.MetaData != Optional.None).Should().BeTrue(); } + [Fact] + public void UpdateRightKey() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); + }); + + + // Change pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device3")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().NotContain(("Device2", 2)); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().Contain(("Device3", 2)); + + + // Remove pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device4")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().NotContain(pair => pair.Key == 2); + + + // Restore pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device2")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().Contain(("Device2", 2)); + + + // No change + _right.AddOrUpdate(new DeviceMetaData(2,"Device2")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.Name, pair.MetaData.ValueOrDefault()?.Key)).Should().Contain(("Device2", 2)); + } + [Fact] public void InitializationWaitsForBothSources() { @@ -240,60 +342,30 @@ public override bool Equals(object? obj) public override string ToString() => $"{Name}"; } - public class DeviceMetaData(string name, bool isAutoConnect = false) : IEquatable + public class DeviceMetaData(int key, string name, bool isAutoConnect = false) : IEquatable { public bool IsAutoConnect { get; } = isAutoConnect; + public int Key { get; } = key; + public string Name { get; set; } = name; - public string Name { get; } = name; - - public static bool operator ==(DeviceMetaData left, DeviceMetaData right) => Equals(left, right); - - public static bool operator !=(DeviceMetaData left, DeviceMetaData right) => !Equals(left, right); + public override string ToString() => $"Key: {Key}. Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; public bool Equals(DeviceMetaData? other) { - if (other is null) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return string.Equals(Name, other.Name) && IsAutoConnect == other.IsAutoConnect; + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return IsAutoConnect == other.IsAutoConnect && Key == other.Key && Name == other.Name; } public override bool Equals(object? obj) { - if (obj is null) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != GetType()) - { - return false; - } - - return Equals((DeviceMetaData)obj); - } - - public override int GetHashCode() - { - unchecked - { - return (Name.GetHashCode() * 397) ^ IsAutoConnect.GetHashCode(); - } + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DeviceMetaData) obj); } - public override string ToString() => $"Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; + public override int GetHashCode() => HashCode.Combine(IsAutoConnect, Key, Name); } public class DeviceWithMetadata(Device device, Optional metaData) : IEquatable diff --git a/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs b/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs index 1fc0748e..dd4b2205 100644 --- a/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs +++ b/src/DynamicData.Tests/Cache/LeftJoinManyFixture.cs @@ -77,6 +77,52 @@ public void Dispose() _result.Dispose(); } + [Fact] + public void RefreshRightKey() + { + _people.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Person() { Name = "Person #1", ParentName = string.Empty } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #3", ParentName = "Person #2" } ); + }); + + var refreshPerson = _people.Lookup("Person #2").Value; + + + // Change pairing + refreshPerson.ParentName = "Person #3"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().NotContain(("Person #1", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #3", "Person #2")); + + + // Remove pairing + refreshPerson.ParentName = "Person #4"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (parentName: family.Parent.Name, chilldName: child.Name))).Should().NotContain(pair => pair.chilldName == "Person #2"); + + + // Restore pairing + refreshPerson.ParentName = "Person #1"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + + + // No change + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + } + [Fact] public void RemoveChild() { @@ -139,6 +185,47 @@ public void UpdateParent() AssertDataIsCorrectlyFormed(updatedPeople); } + [Fact] + public void UpdateRightKey() + { + _people.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Person() { Name = "Person #1", ParentName = string.Empty } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #3", ParentName = "Person #2" } ); + }); + + + // Change pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #3" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().NotContain(("Person #1", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #3", "Person #2")); + + + // Remove pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #4" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (parentName: family.Parent.Name, chilldName: child.Name))).Should().NotContain(pair => pair.chilldName == "Person #2"); + + + // Restore pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + + + // No change + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + } + private void AssertDataIsCorrectlyFormed(Person[] expected, params string[] missingParents) { _result.Data.Count.Should().Be(expected.Length); diff --git a/src/DynamicData.Tests/Cache/RightJoinFixture.cs b/src/DynamicData.Tests/Cache/RightJoinFixture.cs index 653b971d..62aa3019 100644 --- a/src/DynamicData.Tests/Cache/RightJoinFixture.cs +++ b/src/DynamicData.Tests/Cache/RightJoinFixture.cs @@ -114,6 +114,63 @@ public void Dispose() _result.Dispose(); } + [Fact] + public void RefreshRightKey() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); + }); + + var refreshItem = _right.Lookup(2).Value; + + + // Change pairing + refreshItem.Name = "Device3"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain(("Device2", 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain(("Device3", 2)); + + + // Remove pairing + refreshItem.Name = "Device4"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain(("Device3", 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain((null, 2)); + + + // Restore pairing + refreshItem.Name = "Device2"; + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain((null, 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain(("Device2", 2)); + + + // No change + _right.Refresh(refreshItem); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain((null, 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain(("Device2", 2)); + } + [Fact] public void RemoveVarious() { @@ -167,6 +224,58 @@ public void UpdateRight() _result.Data.Items.All(dwm => dwm.Device != Optional.None).Should().BeTrue(); } + [Fact] + public void UpdateRightKey() + { + _left.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Device("Device1")); + innerCache.AddOrUpdate(new Device("Device2")); + innerCache.AddOrUpdate(new Device("Device3")); + }); + + _right.Edit( + innerCache => + { + innerCache.AddOrUpdate(new DeviceMetaData(1,"Device1")); + innerCache.AddOrUpdate(new DeviceMetaData(2,"Device2")); + innerCache.AddOrUpdate(new DeviceMetaData(3,"Device3")); + }); + + + // Change pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device3")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain(("Device2", 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain(("Device3", 2)); + + + // Remove pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device4")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain(("Device3", 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain((null, 2)); + + + // Restore pairing + _right.AddOrUpdate(new DeviceMetaData(2,"Device2")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain((null, 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain(("Device2", 2)); + + + // No change + _right.AddOrUpdate(new DeviceMetaData(2,"Device2")); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().NotContain((null, 2)); + _result.Data.Items.Select(pair => (pair.Device.ValueOrDefault()?.Name, pair.MetaData.Key)).Should().Contain(("Device2", 2)); + } + [Fact] public void MultipleRight() { @@ -293,7 +402,7 @@ public class DeviceMetaData(int key, string name, bool isAutoConnect = false) : { public bool IsAutoConnect { get; } = isAutoConnect; public int Key { get; } = key; - public string Name { get; } = name; + public string Name { get; set; } = name; public override string ToString() => $"Key: {Key}. Metadata: {Name}. IsAutoConnect = {IsAutoConnect}"; diff --git a/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs b/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs index 0f8dc894..24f013c3 100644 --- a/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs +++ b/src/DynamicData.Tests/Cache/RightJoinManyFixture.cs @@ -75,6 +75,55 @@ public void Dispose() _result.Dispose(); } + [Fact] + public void RefreshRightKey() + { + _people.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Person() { Name = "Person #1", ParentName = string.Empty } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #3", ParentName = "Person #2" } ); + }); + + var refreshPerson = _people.Lookup("Person #2").Value; + + + // Change pairing + refreshPerson.ParentName = "Person #3"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain(("Person #1", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain(("Person #3", "Person #2")); + + + // Remove pairing + refreshPerson.ParentName = "Person #4"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain(("Person #3", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain((null, "Person #2")); + + + // Restore pairing + refreshPerson.ParentName = "Person #1"; + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain((null, "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + + + // No change + _people.Refresh(refreshPerson); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain((null, "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + } + [Fact] public void RemoveChild() { @@ -137,6 +186,50 @@ public void UpdateParent() AssertDataIsCorrectlyFormed(updatedPeople); } + [Fact] + public void UpdateRightKey() + { + _people.Edit( + innerCache => + { + innerCache.AddOrUpdate(new Person() { Name = "Person #1", ParentName = string.Empty } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" } ); + innerCache.AddOrUpdate(new Person() { Name = "Person #3", ParentName = "Person #2" } ); + }); + + + // Change pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #3" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain(("Person #1", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain(("Person #3", "Person #2")); + + + // Remove pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #4" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain(("Person #3", "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain((null, "Person #2")); + + + // Restore pairing + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain((null, "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + + + // No change + _people.AddOrUpdate(new Person() { Name = "Person #2", ParentName = "Person #1" }); + + _result.Data.Count.Should().Be(3); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().NotContain((null, "Person #2")); + _result.Data.Items.SelectMany(family => family.Children.Select(child => (family.Parent?.Name, child.Name))).Should().Contain(("Person #1", "Person #2")); + } + private void AssertDataIsCorrectlyFormed(Person[] allPeople, params string[] missingParents) { var grouped = allPeople.GroupBy(p => p.ParentName).Where(p => p.Any() && !missingParents.Contains(p.Key)).AsArray(); diff --git a/src/DynamicData.Tests/Domain/Person.cs b/src/DynamicData.Tests/Domain/Person.cs index 0018cbba..bca018cb 100644 --- a/src/DynamicData.Tests/Domain/Person.cs +++ b/src/DynamicData.Tests/Domain/Person.cs @@ -22,6 +22,7 @@ public class Person : AbstractNotifyPropertyChanged, IEquatable, ICompar private int _age; private int? _ageNullable; private Color _favoriteColor; + private string _parentName; private AnimalFamily _petType; public Person() @@ -39,7 +40,7 @@ public Person(string name, int age, string gender = "F", string? parentName = nu Name = name; _age = age; Gender = gender; - ParentName = parentName ?? string.Empty; + _parentName = parentName ?? string.Empty; } public Person(string name, int? age, string gender = "F", string? parentName = null) @@ -47,7 +48,7 @@ public Person(string name, int? age, string gender = "F", string? parentName = n Name = name; _ageNullable = age; Gender = gender; - ParentName = parentName ?? string.Empty; + _parentName = parentName ?? string.Empty; } private Person(string name, int? age, string gender, Person personCopyKey) @@ -55,7 +56,7 @@ private Person(string name, int? age, string gender, Person personCopyKey) Name = name; _ageNullable = age; Gender = gender; - ParentName = personCopyKey?.ParentName ?? throw new ArgumentNullException(nameof(personCopyKey)); + _parentName = personCopyKey?.ParentName ?? throw new ArgumentNullException(nameof(personCopyKey)); UniqueKey = personCopyKey.UniqueKey; } @@ -93,9 +94,13 @@ public AnimalFamily PetType public string UniqueKey { get; } = Guid.NewGuid().ToString("B"); - public string Name { get; } + public string Name { get; init; } - public string ParentName { get; } + public string ParentName + { + get => _parentName; + set => SetAndRaise(ref _parentName, value); + } public static bool operator ==(Person left, Person right) => Equals(left, right); diff --git a/src/DynamicData/Cache/Internal/InnerJoin.cs b/src/DynamicData/Cache/Internal/InnerJoin.cs index edff85f5..23752cb0 100644 --- a/src/DynamicData/Cache/Internal/InnerJoin.cs +++ b/src/DynamicData/Cache/Internal/InnerJoin.cs @@ -34,6 +34,8 @@ internal sealed class InnerJoin(); + // joined is the final cache var joinedCache = new ChangeAwareCache(); @@ -93,28 +95,55 @@ internal sealed class InnerJoin.Default.Equals(leftKey, oldLeftKey) + && leftCache.Lookup(oldLeftKey).HasValue) + { + joinedCache.Remove((oldLeftKey, change.Key)); + } + + // If the new item has a pairing, either add or update it + rightForeignKeysByKey[change.Key] = leftKey; + var right = change.Current; + var left = leftCache.Lookup(leftKey); + if (left.HasValue) + { + joinedCache.AddOrUpdate(_resultSelector((leftKey, change.Key), left.Value, right), (leftKey, change.Key)); + } } - break; case ChangeReason.Remove: // remove from result because a right value is expected + rightForeignKeysByKey.Remove(change.Key); joinedCache.Remove((leftKey, change.Key)); break; case ChangeReason.Refresh: - // propagate upstream - joinedCache.Refresh((leftKey, change.Key)); + { + // Check to see if the foreign key has changed, and re-pair the item, if so + var oldLeftKey = rightForeignKeysByKey[change.Key]; + rightForeignKeysByKey[change.Key] = leftKey; + if (!EqualityComparer.Default.Equals(leftKey, oldLeftKey)) + { + if (leftCache.Lookup(oldLeftKey).HasValue) + { + joinedCache.Remove((oldLeftKey, change.Key)); + } + + var left = leftCache.Lookup(leftKey); + if (left.HasValue) + { + joinedCache.AddOrUpdate(_resultSelector((leftKey, change.Key), left.Value, change.Current), (leftKey, change.Key)); + } + } + else + { + joinedCache.Refresh((leftKey, change.Key)); + } + } break; } } diff --git a/src/DynamicData/Cache/Internal/LeftJoin.cs b/src/DynamicData/Cache/Internal/LeftJoin.cs index 37e1fa17..5ab305ba 100644 --- a/src/DynamicData/Cache/Internal/LeftJoin.cs +++ b/src/DynamicData/Cache/Internal/LeftJoin.cs @@ -30,7 +30,12 @@ public IObservable> Run() => Observable.Creat // create local backing stores var leftShare = _left.Synchronize(locker).Publish(); var leftCache = leftShare.AsObservableCache(false); - var rightCache = _right.Synchronize(locker).ChangeKey(_rightKeySelector).AsObservableCache(false); + + var rightShare = _right.Synchronize(locker).Publish(); + var rightCache = rightShare.AsObservableCache(false); + var rightForeignCache = rightShare.ChangeKey(_rightKeySelector).AsObservableCache(false); + + var rightForeignKeysByKey = new Dictionary(); // joined is the final cache var joined = new ChangeAwareCache(); @@ -48,7 +53,7 @@ public IObservable> Run() => Observable.Creat case ChangeReason.Update: // Update with left (and right if it is presents) var leftCurrent = change.Current; - var rightLookup = rightCache.Lookup(change.Key); + var rightLookup = rightForeignCache.Lookup(change.Key); joined.AddOrUpdate(_resultSelector(change.Key, leftCurrent, rightLookup), change.Key); break; @@ -73,46 +78,57 @@ public IObservable> Run() => Observable.Creat foreach (var change in changes.ToConcreteType()) { var right = change.Current; - var left = leftCache.Lookup(change.Key); + var foreignKey = _rightKeySelector.Invoke(change.Current); + var left = leftCache.Lookup(foreignKey); switch (change.Reason) { case ChangeReason.Add: case ChangeReason.Update: { - if (left.HasValue) - { - // Update with left and right value - joined.AddOrUpdate(_resultSelector(change.Key, left.Value, right), change.Key); - } - else + if (rightForeignKeysByKey.TryGetValue(change.Key, out var priorForeignKey) + && !EqualityComparer.Default.Equals(foreignKey, priorForeignKey)) { - // remove if it is already in the cache - joined.Remove(change.Key); + var priorLeft = leftCache.Lookup(priorForeignKey); + if (priorLeft.HasValue) + joined.AddOrUpdate(_resultSelector(priorForeignKey, priorLeft.Value, Optional.None), priorForeignKey); } + + if (left.HasValue) + joined.AddOrUpdate(_resultSelector(foreignKey, left.Value, right), foreignKey); + + rightForeignKeysByKey[change.Key] = foreignKey; } break; case ChangeReason.Remove: + if (left.HasValue) + joined.AddOrUpdate(_resultSelector(foreignKey, left.Value, Optional.None), foreignKey); + + rightForeignKeysByKey.Remove(change.Key); + + break; + + case ChangeReason.Refresh: { - if (left.HasValue) + if (rightForeignKeysByKey.TryGetValue(change.Key, out var priorForeignKey) + && !EqualityComparer.Default.Equals(foreignKey, priorForeignKey)) { - // Update with no right value - joined.AddOrUpdate(_resultSelector(change.Key, left.Value, Optional.None), change.Key); + var priorLeft = leftCache.Lookup(priorForeignKey); + if (priorLeft.HasValue) + joined.AddOrUpdate(_resultSelector(priorForeignKey, priorLeft.Value, Optional.None), priorForeignKey); + + if (left.HasValue) + joined.AddOrUpdate(_resultSelector(foreignKey, left.Value, right), foreignKey); + + rightForeignKeysByKey[change.Key] = foreignKey; } else { - // remove if it is already in the cache - joined.Remove(change.Key); + joined.Refresh(foreignKey); } } - - break; - - case ChangeReason.Refresh: - // propagate upstream - joined.Refresh(change.Key); break; } } @@ -126,9 +142,11 @@ public IObservable> Run() => Observable.Creat { var observerSubscription = leftLoader.Merge(rightLoader).SubscribeSafe(observer); + var rightShareConnection = rightShare.Connect(); + hasInitialized = true; - return new CompositeDisposable(observerSubscription, leftCache, rightCache, leftShare.Connect()); + return new CompositeDisposable(observerSubscription, leftCache, rightCache, rightShareConnection, leftShare.Connect()); } }); } diff --git a/src/DynamicData/Cache/Internal/RightJoin.cs b/src/DynamicData/Cache/Internal/RightJoin.cs index 313aff49..74d42d91 100644 --- a/src/DynamicData/Cache/Internal/RightJoin.cs +++ b/src/DynamicData/Cache/Internal/RightJoin.cs @@ -33,7 +33,12 @@ public IObservable> Run() => Observable.Crea var rightShare = _right.Synchronize(locker).Publish(); var rightCache = rightShare.AsObservableCache(false); - var rightGrouped = rightShare.GroupWithImmutableState(_rightKeySelector).AsObservableCache(false); + var rightForeignCache = rightShare + .Transform(static (item, key) => (item, key)) + .ChangeKey(pair => _rightKeySelector.Invoke(pair.item)) + .AsObservableCache(false); + + var rightForeignKeysByKey = new Dictionary(); // joined is the final cache var joinedCache = new ChangeAwareCache(); @@ -44,25 +49,38 @@ public IObservable> Run() => Observable.Crea { foreach (var change in changes.ToConcreteType()) { - var leftKey = _rightKeySelector(change.Current); + var foreignKey = _rightKeySelector(change.Current); switch (change.Reason) { case ChangeReason.Add: case ChangeReason.Update: // Update with right (and right if it is presents) var rightCurrent = change.Current; - var leftLookup = leftCache.Lookup(leftKey); + var leftLookup = leftCache.Lookup(foreignKey); joinedCache.AddOrUpdate(_resultSelector(change.Key, leftLookup, rightCurrent), change.Key); + + rightForeignKeysByKey[change.Key] = foreignKey; break; case ChangeReason.Remove: // remove from result because a right value is expected joinedCache.Remove(change.Key); + rightForeignKeysByKey.Remove(change.Key); break; case ChangeReason.Refresh: - // propagate upstream - joinedCache.Refresh(change.Key); + if (rightForeignKeysByKey.TryGetValue(change.Key, out var priorForeignKey) + && !EqualityComparer.Default.Equals(foreignKey, priorForeignKey)) + { + joinedCache.AddOrUpdate(_resultSelector(change.Key, leftCache.Lookup(foreignKey), change.Current), change.Key); + + rightForeignKeysByKey[change.Key] = foreignKey; + } + else + { + // propagate downstream + joinedCache.Refresh(change.Key); + } break; } } @@ -76,7 +94,7 @@ public IObservable> Run() => Observable.Crea foreach (var change in changes.ToConcreteType()) { var left = change.Current; - var right = rightGrouped.Lookup(change.Key); + var right = rightForeignCache.Lookup(change.Key); if (right.HasValue) { @@ -84,25 +102,25 @@ public IObservable> Run() => Observable.Crea { case ChangeReason.Add: case ChangeReason.Update: - foreach (var keyvalue in right.Value.KeyValues) + if (right.HasValue) { - joinedCache.AddOrUpdate(_resultSelector(keyvalue.Key, left, keyvalue.Value), keyvalue.Key); + joinedCache.AddOrUpdate(_resultSelector(right.Value.key!, left, right.Value.item), right.Value.key); } break; case ChangeReason.Remove: - foreach (var keyvalue in right.Value.KeyValues) + if (right.HasValue) { - joinedCache.AddOrUpdate(_resultSelector(keyvalue.Key, Optional.None, keyvalue.Value), keyvalue.Key); + joinedCache.AddOrUpdate(_resultSelector(right.Value.key, Optional.None, right.Value.item), right.Value.key); } break; case ChangeReason.Refresh: - foreach (var key in right.Value.Keys) + if (right.HasValue) { - joinedCache.Refresh(key); + joinedCache.Refresh(right.Value.key); } break;