From a76d5fe29a5611cd66c116910145d84d65ff8603 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 16 Oct 2025 15:56:31 -0500 Subject: [PATCH] Fix LWWDictionary.Delta ArgumentNullException when underlying delta is null (#7910) (#7911) ## Summary Fixed a bug where LWWDictionary.Delta would throw ArgumentNullException when the underlying ORDictionary.Delta is null, which is a legitimate state after initialization or calling ResetDelta(). ## Changes - Modified LWWDictionary.Delta property to return null when Underlying.Delta is null - Added test Bugfix_7910_LWWDictionary_Delta_should_handle_null_underlying_delta to verify the fix ## Root Cause The LWWDictionary.Delta property was unconditionally wrapping Underlying.Delta in a LWWDictionaryDelta constructor, which throws ArgumentNullException when passed null. However, ORDictionary.Delta can legitimately return null after construction or ResetDelta(). The Replicator expects deltas to be nullable (uses ?? operator at lines 665 and 685), so this fix aligns LWWDictionary with the expected interface contract. Fixes #7910 --- .../LWWDictionarySpec.cs | 28 +++++++++++++++++++ .../Akka.DistributedData/LWWDictionary.cs | 4 +-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/contrib/cluster/Akka.DistributedData.Tests/LWWDictionarySpec.cs b/src/contrib/cluster/Akka.DistributedData.Tests/LWWDictionarySpec.cs index 0b659512566..127cf7d96fa 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests/LWWDictionarySpec.cs +++ b/src/contrib/cluster/Akka.DistributedData.Tests/LWWDictionarySpec.cs @@ -161,5 +161,33 @@ public async Task Bugfix_4400_LWWDictionary_Deltas_must_merge_other_LWWDictionar merged1.Entries["b"].Should().BeEquivalentTo("B2"); merged1.Entries["c"].Should().BeEquivalentTo("C"); } + + /// + /// Bug reproduction: https://github.com/akkadotnet/akka.net/issues/7910 + /// LWWDictionary.Delta should return null when underlying ORDictionary.Delta is null + /// + [Fact] + public void Bugfix_7910_LWWDictionary_Delta_should_handle_null_underlying_delta() + { + // Empty dictionary has no delta + var empty = LWWDictionary.Empty; + empty.Delta.Should().BeNull("empty dictionary should have null delta"); + + // After ResetDelta(), delta should be null + var m1 = LWWDictionary.Empty + .SetItem(_node1, "a", "A") + .SetItem(_node1, "b", "B"); + + m1.Delta.Should().NotBeNull("dictionary with modifications should have a delta"); + + var m2 = m1.ResetDelta(); + m2.Delta.Should().BeNull("after ResetDelta(), delta should be null"); + + // Verify the dictionary still contains the data + m2.ContainsKey("a").Should().BeTrue(); + m2.ContainsKey("b").Should().BeTrue(); + m2["a"].Should().Be("A"); + m2["b"].Should().Be("B"); + } } } diff --git a/src/contrib/cluster/Akka.DistributedData/LWWDictionary.cs b/src/contrib/cluster/Akka.DistributedData/LWWDictionary.cs index 5562e88dd53..b556b7554ba 100644 --- a/src/contrib/cluster/Akka.DistributedData/LWWDictionary.cs +++ b/src/contrib/cluster/Akka.DistributedData/LWWDictionary.cs @@ -402,8 +402,8 @@ public override int GetHashCode() } // TODO: optimize this so it doesn't allocate each time it's called - public ORDictionary>.IDeltaOperation Delta => - new LWWDictionaryDelta(Underlying.Delta); + public ORDictionary>.IDeltaOperation Delta => + Underlying.Delta == null ? null : new LWWDictionaryDelta(Underlying.Delta); IReplicatedDelta IDeltaReplicatedData.Delta => Delta;