From dbfa515546ec6c4dd37dd5fd69538a31e0a9fa25 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Fri, 10 Apr 2026 00:30:31 -0700 Subject: [PATCH 1/2] Refactor SwappableLock to support NET9+ Lock type Add Lock overloads for SwappableLock.SwapTo and constructor to support the new System.Threading.Lock type on .NET 9+. Uses #if NET9_0_OR_GREATER conditional compilation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/DynamicData/Internal/SwappableLock.cs | 56 ++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/DynamicData/Internal/SwappableLock.cs b/src/DynamicData/Internal/SwappableLock.cs index 267607e9b..5f1f47599 100644 --- a/src/DynamicData/Internal/SwappableLock.cs +++ b/src/DynamicData/Internal/SwappableLock.cs @@ -18,23 +18,73 @@ public static SwappableLock CreateAndEnter(object gate) return result; } +#if NET9_0_OR_GREATER + public static SwappableLock CreateAndEnter(Lock gate) + { + gate.Enter(); + return new SwappableLock() { _lockGate = gate }; + } +#endif + public void SwapTo(object gate) { +#if NET9_0_OR_GREATER + if (_gate is null && _lockGate is null) + throw new InvalidOperationException("Lock is not initialized"); +#else if (_gate is null) throw new InvalidOperationException("Lock is not initialized"); +#endif var hasNewLock = false; Monitor.Enter(gate, ref hasNewLock); +#if NET9_0_OR_GREATER + if (_lockGate is not null) + { + _lockGate.Exit(); + _lockGate = null; + } + else +#endif if (_hasLock) - Monitor.Exit(_gate); + { + Monitor.Exit(_gate!); + } _hasLock = hasNewLock; _gate = gate; } +#if NET9_0_OR_GREATER + public void SwapTo(Lock gate) + { + if (_lockGate is null && _gate is null) + throw new InvalidOperationException("Lock is not initialized"); + + gate.Enter(); + + if (_lockGate is not null) + _lockGate.Exit(); + else if (_hasLock) + Monitor.Exit(_gate!); + + _lockGate = gate; + _hasLock = false; + _gate = null; + } +#endif + public void Dispose() { +#if NET9_0_OR_GREATER + if (_lockGate is not null) + { + _lockGate.Exit(); + _lockGate = null; + } + else +#endif if (_hasLock && (_gate is not null)) { Monitor.Exit(_gate); @@ -45,4 +95,8 @@ public void Dispose() private bool _hasLock; private object? _gate; + +#if NET9_0_OR_GREATER + private Lock? _lockGate; +#endif } From b57ea6193725bfa1c1ea13d789f5e744b96ac6fd Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Mon, 13 Apr 2026 10:18:05 -0700 Subject: [PATCH 2/2] refactor: split SwappableLock into two clean TFM implementations, add tests Replace #if spaghetti throughout every method with two complete, independent implementations behind a single top-level #if NET9_0_OR_GREATER. NET9: uses System.Threading.Lock directly (simpler, no _hasLock bool needed). Pre-NET9: uses Monitor.Enter/Exit on object gates (unchanged behavior). Filter.Dynamic: gates upgraded to Lock on NET9 for consistency. Added SwappableLockFixture with 7 tests covering CreateAndEnter, Dispose (release + idempotent), SwapTo (basic + chained), uninitialized throws, and Dispose after swap. --- .../Internal/SwappableLockFixture.cs | 214 ++++++++++++++++++ .../Cache/Internal/Filter.Dynamic.cs | 13 ++ src/DynamicData/Internal/SwappableLock.cs | 88 +++---- 3 files changed, 263 insertions(+), 52 deletions(-) create mode 100644 src/DynamicData.Tests/Internal/SwappableLockFixture.cs diff --git a/src/DynamicData.Tests/Internal/SwappableLockFixture.cs b/src/DynamicData.Tests/Internal/SwappableLockFixture.cs new file mode 100644 index 000000000..4b3bd980e --- /dev/null +++ b/src/DynamicData.Tests/Internal/SwappableLockFixture.cs @@ -0,0 +1,214 @@ +// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved. +// Roland Pheasant licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Threading; +using FluentAssertions; +using Xunit; + +namespace DynamicData.Tests.Internal; + +public sealed class SwappableLockFixture +{ +#if NET9_0_OR_GREATER + + [Fact] + public void CreateAndEnter_AcquiresLock() + { + var gate = new Lock(); + + using var swappable = SwappableLock.CreateAndEnter(gate); + + gate.IsHeldByCurrentThread.Should().BeTrue(); + } + + [Fact] + public void Dispose_ReleasesLock() + { + var gate = new Lock(); + var swappable = SwappableLock.CreateAndEnter(gate); + + swappable.Dispose(); + + gate.IsHeldByCurrentThread.Should().BeFalse(); + } + + [Fact] + public void Dispose_IsIdempotent() + { + var gate = new Lock(); + var swappable = SwappableLock.CreateAndEnter(gate); + + swappable.Dispose(); + swappable.Dispose(); + + gate.IsHeldByCurrentThread.Should().BeFalse(); + } + + [Fact] + public void SwapTo_AcquiresNewAndReleasesOld() + { + var first = new Lock(); + var second = new Lock(); + + using var swappable = SwappableLock.CreateAndEnter(first); + swappable.SwapTo(second); + + first.IsHeldByCurrentThread.Should().BeFalse(); + second.IsHeldByCurrentThread.Should().BeTrue(); + } + + [Fact] + public void SwapTo_ChainedSwaps() + { + var a = new Lock(); + var b = new Lock(); + var c = new Lock(); + + using var swappable = SwappableLock.CreateAndEnter(a); + swappable.SwapTo(b); + swappable.SwapTo(c); + + a.IsHeldByCurrentThread.Should().BeFalse(); + b.IsHeldByCurrentThread.Should().BeFalse(); + c.IsHeldByCurrentThread.Should().BeTrue(); + } + + [Fact] + public void SwapTo_WithoutCreate_Throws() + { + var gate = new Lock(); + var swappable = new SwappableLock(); + + try + { + swappable.SwapTo(gate); + throw new Xunit.Sdk.XunitException("Expected InvalidOperationException"); + } + catch (InvalidOperationException) + { + } + } + + [Fact] + public void Dispose_AfterSwap_ReleasesSwappedLock() + { + var first = new Lock(); + var second = new Lock(); + + var swappable = SwappableLock.CreateAndEnter(first); + swappable.SwapTo(second); + swappable.Dispose(); + + first.IsHeldByCurrentThread.Should().BeFalse(); + second.IsHeldByCurrentThread.Should().BeFalse(); + } + +#else + + [Fact] + public void CreateAndEnter_AcquiresLock() + { + var gate = new object(); + + using var swappable = SwappableLock.CreateAndEnter(gate); + + Monitor.IsEntered(gate).Should().BeTrue(); + } + + [Fact] + public void Dispose_ReleasesLock() + { + var gate = new object(); + var swappable = SwappableLock.CreateAndEnter(gate); + + swappable.Dispose(); + + Monitor.IsEntered(gate).Should().BeFalse(); + } + + [Fact] + public void Dispose_IsIdempotent() + { + var gate = new object(); + var swappable = SwappableLock.CreateAndEnter(gate); + + swappable.Dispose(); + swappable.Dispose(); + + Monitor.IsEntered(gate).Should().BeFalse(); + } + + [Fact] + public void SwapTo_AcquiresNewAndReleasesOld() + { + var first = new object(); + var second = new object(); + + using var swappable = SwappableLock.CreateAndEnter(first); + swappable.SwapTo(second); + + Monitor.IsEntered(first).Should().BeFalse(); + Monitor.IsEntered(second).Should().BeTrue(); + } + + [Fact] + public void SwapTo_ChainedSwaps() + { + var a = new object(); + var b = new object(); + var c = new object(); + + using var swappable = SwappableLock.CreateAndEnter(a); + swappable.SwapTo(b); + swappable.SwapTo(c); + + Monitor.IsEntered(a).Should().BeFalse(); + Monitor.IsEntered(b).Should().BeFalse(); + Monitor.IsEntered(c).Should().BeTrue(); + } + + [Fact] + public void SwapTo_WithoutCreate_Throws() + { + var gate = new object(); + var swappable = new SwappableLock(); + + try + { + swappable.SwapTo(gate); + throw new Xunit.Sdk.XunitException("Expected InvalidOperationException"); + } + catch (InvalidOperationException) + { + } + } + + [Fact] + public void Dispose_AfterSwap_ReleasesSwappedLock() + { + var first = new object(); + var second = new object(); + + var swappable = SwappableLock.CreateAndEnter(first); + swappable.SwapTo(second); + swappable.Dispose(); + + Monitor.IsEntered(first).Should().BeFalse(); + Monitor.IsEntered(second).Should().BeFalse(); + } + + [Fact] + public void SwapTo_SameLock_WorksWithReentrantMonitor() + { + var gate = new object(); + + using var swappable = SwappableLock.CreateAndEnter(gate); + swappable.SwapTo(gate); + + Monitor.IsEntered(gate).Should().BeTrue(); + } + +#endif +} diff --git a/src/DynamicData/Cache/Internal/Filter.Dynamic.cs b/src/DynamicData/Cache/Internal/Filter.Dynamic.cs index a6b38bc96..9727c04a3 100644 --- a/src/DynamicData/Cache/Internal/Filter.Dynamic.cs +++ b/src/DynamicData/Cache/Internal/Filter.Dynamic.cs @@ -47,6 +47,11 @@ private sealed class Subscription private readonly IDisposable? _sourceSubscription; private readonly bool _suppressEmptyChangeSets; +#if NET9_0_OR_GREATER + private readonly Lock _downstreamGate = new(); + private readonly Lock _upstreamGate = new(); +#endif + private bool _hasInitialized; private bool _hasPredicateStateCompleted; private bool _hasReapplyFilterCompleted; @@ -116,11 +121,19 @@ public void Dispose() _sourceSubscription?.Dispose(); } +#if NET9_0_OR_GREATER + private Lock DownstreamSynchronizationGate + => _downstreamGate; + + private Lock UpstreamSynchronizationGate + => _upstreamGate; +#else private object DownstreamSynchronizationGate => _downstreamChangesBuffer; private object UpstreamSynchronizationGate => _itemStatesByKey; +#endif private ChangeSet AssembleDownstreamChanges() { diff --git a/src/DynamicData/Internal/SwappableLock.cs b/src/DynamicData/Internal/SwappableLock.cs index 5f1f47599..6b9fd31f3 100644 --- a/src/DynamicData/Internal/SwappableLock.cs +++ b/src/DynamicData/Internal/SwappableLock.cs @@ -4,6 +4,40 @@ namespace DynamicData; +#if NET9_0_OR_GREATER + +internal ref struct SwappableLock +{ + public static SwappableLock CreateAndEnter(Lock gate) + { + gate.Enter(); + return new SwappableLock { _gate = gate }; + } + + public void SwapTo(Lock gate) + { + if (_gate is null) + throw new InvalidOperationException("Lock is not initialized"); + + gate.Enter(); + _gate.Exit(); + _gate = gate; + } + + public void Dispose() + { + if (_gate is not null) + { + _gate.Exit(); + _gate = null; + } + } + + private Lock? _gate; +} + +#else + internal ref struct SwappableLock { public static SwappableLock CreateAndEnter(object gate) @@ -18,73 +52,25 @@ public static SwappableLock CreateAndEnter(object gate) return result; } -#if NET9_0_OR_GREATER - public static SwappableLock CreateAndEnter(Lock gate) - { - gate.Enter(); - return new SwappableLock() { _lockGate = gate }; - } -#endif - public void SwapTo(object gate) { -#if NET9_0_OR_GREATER - if (_gate is null && _lockGate is null) - throw new InvalidOperationException("Lock is not initialized"); -#else if (_gate is null) throw new InvalidOperationException("Lock is not initialized"); -#endif var hasNewLock = false; Monitor.Enter(gate, ref hasNewLock); -#if NET9_0_OR_GREATER - if (_lockGate is not null) - { - _lockGate.Exit(); - _lockGate = null; - } - else -#endif if (_hasLock) { - Monitor.Exit(_gate!); + Monitor.Exit(_gate); } _hasLock = hasNewLock; _gate = gate; } -#if NET9_0_OR_GREATER - public void SwapTo(Lock gate) - { - if (_lockGate is null && _gate is null) - throw new InvalidOperationException("Lock is not initialized"); - - gate.Enter(); - - if (_lockGate is not null) - _lockGate.Exit(); - else if (_hasLock) - Monitor.Exit(_gate!); - - _lockGate = gate; - _hasLock = false; - _gate = null; - } -#endif - public void Dispose() { -#if NET9_0_OR_GREATER - if (_lockGate is not null) - { - _lockGate.Exit(); - _lockGate = null; - } - else -#endif if (_hasLock && (_gate is not null)) { Monitor.Exit(_gate); @@ -95,8 +81,6 @@ public void Dispose() private bool _hasLock; private object? _gate; +} -#if NET9_0_OR_GREATER - private Lock? _lockGate; #endif -}