Skip to content
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
214 changes: 214 additions & 0 deletions src/DynamicData.Tests/Internal/SwappableLockFixture.cs
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 13 additions & 0 deletions src/DynamicData/Cache/Internal/Filter.Dynamic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TObject, TKey> AssembleDownstreamChanges()
{
Expand Down
38 changes: 38 additions & 0 deletions src/DynamicData/Internal/SwappableLock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -27,7 +61,9 @@ public void SwapTo(object gate)
Monitor.Enter(gate, ref hasNewLock);

if (_hasLock)
{
Monitor.Exit(_gate);
}

_hasLock = hasNewLock;
_gate = gate;
Expand All @@ -46,3 +82,5 @@ public void Dispose()
private bool _hasLock;
private object? _gate;
}

#endif
Loading