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 267607e9b..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) @@ -27,7 +61,9 @@ public void SwapTo(object gate) Monitor.Enter(gate, ref hasNewLock); if (_hasLock) + { Monitor.Exit(_gate); + } _hasLock = hasNewLock; _gate = gate; @@ -46,3 +82,5 @@ public void Dispose() private bool _hasLock; private object? _gate; } + +#endif