Skip to content

Commit b0d4c70

Browse files
authored
Update SwappableLock to support NET9+ Lock type (#1077)
* 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. * 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.
1 parent 12c1947 commit b0d4c70

3 files changed

Lines changed: 265 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
2+
// Roland Pheasant licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System;
6+
using System.Threading;
7+
using FluentAssertions;
8+
using Xunit;
9+
10+
namespace DynamicData.Tests.Internal;
11+
12+
public sealed class SwappableLockFixture
13+
{
14+
#if NET9_0_OR_GREATER
15+
16+
[Fact]
17+
public void CreateAndEnter_AcquiresLock()
18+
{
19+
var gate = new Lock();
20+
21+
using var swappable = SwappableLock.CreateAndEnter(gate);
22+
23+
gate.IsHeldByCurrentThread.Should().BeTrue();
24+
}
25+
26+
[Fact]
27+
public void Dispose_ReleasesLock()
28+
{
29+
var gate = new Lock();
30+
var swappable = SwappableLock.CreateAndEnter(gate);
31+
32+
swappable.Dispose();
33+
34+
gate.IsHeldByCurrentThread.Should().BeFalse();
35+
}
36+
37+
[Fact]
38+
public void Dispose_IsIdempotent()
39+
{
40+
var gate = new Lock();
41+
var swappable = SwappableLock.CreateAndEnter(gate);
42+
43+
swappable.Dispose();
44+
swappable.Dispose();
45+
46+
gate.IsHeldByCurrentThread.Should().BeFalse();
47+
}
48+
49+
[Fact]
50+
public void SwapTo_AcquiresNewAndReleasesOld()
51+
{
52+
var first = new Lock();
53+
var second = new Lock();
54+
55+
using var swappable = SwappableLock.CreateAndEnter(first);
56+
swappable.SwapTo(second);
57+
58+
first.IsHeldByCurrentThread.Should().BeFalse();
59+
second.IsHeldByCurrentThread.Should().BeTrue();
60+
}
61+
62+
[Fact]
63+
public void SwapTo_ChainedSwaps()
64+
{
65+
var a = new Lock();
66+
var b = new Lock();
67+
var c = new Lock();
68+
69+
using var swappable = SwappableLock.CreateAndEnter(a);
70+
swappable.SwapTo(b);
71+
swappable.SwapTo(c);
72+
73+
a.IsHeldByCurrentThread.Should().BeFalse();
74+
b.IsHeldByCurrentThread.Should().BeFalse();
75+
c.IsHeldByCurrentThread.Should().BeTrue();
76+
}
77+
78+
[Fact]
79+
public void SwapTo_WithoutCreate_Throws()
80+
{
81+
var gate = new Lock();
82+
var swappable = new SwappableLock();
83+
84+
try
85+
{
86+
swappable.SwapTo(gate);
87+
throw new Xunit.Sdk.XunitException("Expected InvalidOperationException");
88+
}
89+
catch (InvalidOperationException)
90+
{
91+
}
92+
}
93+
94+
[Fact]
95+
public void Dispose_AfterSwap_ReleasesSwappedLock()
96+
{
97+
var first = new Lock();
98+
var second = new Lock();
99+
100+
var swappable = SwappableLock.CreateAndEnter(first);
101+
swappable.SwapTo(second);
102+
swappable.Dispose();
103+
104+
first.IsHeldByCurrentThread.Should().BeFalse();
105+
second.IsHeldByCurrentThread.Should().BeFalse();
106+
}
107+
108+
#else
109+
110+
[Fact]
111+
public void CreateAndEnter_AcquiresLock()
112+
{
113+
var gate = new object();
114+
115+
using var swappable = SwappableLock.CreateAndEnter(gate);
116+
117+
Monitor.IsEntered(gate).Should().BeTrue();
118+
}
119+
120+
[Fact]
121+
public void Dispose_ReleasesLock()
122+
{
123+
var gate = new object();
124+
var swappable = SwappableLock.CreateAndEnter(gate);
125+
126+
swappable.Dispose();
127+
128+
Monitor.IsEntered(gate).Should().BeFalse();
129+
}
130+
131+
[Fact]
132+
public void Dispose_IsIdempotent()
133+
{
134+
var gate = new object();
135+
var swappable = SwappableLock.CreateAndEnter(gate);
136+
137+
swappable.Dispose();
138+
swappable.Dispose();
139+
140+
Monitor.IsEntered(gate).Should().BeFalse();
141+
}
142+
143+
[Fact]
144+
public void SwapTo_AcquiresNewAndReleasesOld()
145+
{
146+
var first = new object();
147+
var second = new object();
148+
149+
using var swappable = SwappableLock.CreateAndEnter(first);
150+
swappable.SwapTo(second);
151+
152+
Monitor.IsEntered(first).Should().BeFalse();
153+
Monitor.IsEntered(second).Should().BeTrue();
154+
}
155+
156+
[Fact]
157+
public void SwapTo_ChainedSwaps()
158+
{
159+
var a = new object();
160+
var b = new object();
161+
var c = new object();
162+
163+
using var swappable = SwappableLock.CreateAndEnter(a);
164+
swappable.SwapTo(b);
165+
swappable.SwapTo(c);
166+
167+
Monitor.IsEntered(a).Should().BeFalse();
168+
Monitor.IsEntered(b).Should().BeFalse();
169+
Monitor.IsEntered(c).Should().BeTrue();
170+
}
171+
172+
[Fact]
173+
public void SwapTo_WithoutCreate_Throws()
174+
{
175+
var gate = new object();
176+
var swappable = new SwappableLock();
177+
178+
try
179+
{
180+
swappable.SwapTo(gate);
181+
throw new Xunit.Sdk.XunitException("Expected InvalidOperationException");
182+
}
183+
catch (InvalidOperationException)
184+
{
185+
}
186+
}
187+
188+
[Fact]
189+
public void Dispose_AfterSwap_ReleasesSwappedLock()
190+
{
191+
var first = new object();
192+
var second = new object();
193+
194+
var swappable = SwappableLock.CreateAndEnter(first);
195+
swappable.SwapTo(second);
196+
swappable.Dispose();
197+
198+
Monitor.IsEntered(first).Should().BeFalse();
199+
Monitor.IsEntered(second).Should().BeFalse();
200+
}
201+
202+
[Fact]
203+
public void SwapTo_SameLock_WorksWithReentrantMonitor()
204+
{
205+
var gate = new object();
206+
207+
using var swappable = SwappableLock.CreateAndEnter(gate);
208+
swappable.SwapTo(gate);
209+
210+
Monitor.IsEntered(gate).Should().BeTrue();
211+
}
212+
213+
#endif
214+
}

src/DynamicData/Cache/Internal/Filter.Dynamic.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ private sealed class Subscription
4747
private readonly IDisposable? _sourceSubscription;
4848
private readonly bool _suppressEmptyChangeSets;
4949

50+
#if NET9_0_OR_GREATER
51+
private readonly Lock _downstreamGate = new();
52+
private readonly Lock _upstreamGate = new();
53+
#endif
54+
5055
private bool _hasInitialized;
5156
private bool _hasPredicateStateCompleted;
5257
private bool _hasReapplyFilterCompleted;
@@ -116,11 +121,19 @@ public void Dispose()
116121
_sourceSubscription?.Dispose();
117122
}
118123

124+
#if NET9_0_OR_GREATER
125+
private Lock DownstreamSynchronizationGate
126+
=> _downstreamGate;
127+
128+
private Lock UpstreamSynchronizationGate
129+
=> _upstreamGate;
130+
#else
119131
private object DownstreamSynchronizationGate
120132
=> _downstreamChangesBuffer;
121133

122134
private object UpstreamSynchronizationGate
123135
=> _itemStatesByKey;
136+
#endif
124137

125138
private ChangeSet<TObject, TKey> AssembleDownstreamChanges()
126139
{

src/DynamicData/Internal/SwappableLock.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,40 @@
44

55
namespace DynamicData;
66

7+
#if NET9_0_OR_GREATER
8+
9+
internal ref struct SwappableLock
10+
{
11+
public static SwappableLock CreateAndEnter(Lock gate)
12+
{
13+
gate.Enter();
14+
return new SwappableLock { _gate = gate };
15+
}
16+
17+
public void SwapTo(Lock gate)
18+
{
19+
if (_gate is null)
20+
throw new InvalidOperationException("Lock is not initialized");
21+
22+
gate.Enter();
23+
_gate.Exit();
24+
_gate = gate;
25+
}
26+
27+
public void Dispose()
28+
{
29+
if (_gate is not null)
30+
{
31+
_gate.Exit();
32+
_gate = null;
33+
}
34+
}
35+
36+
private Lock? _gate;
37+
}
38+
39+
#else
40+
741
internal ref struct SwappableLock
842
{
943
public static SwappableLock CreateAndEnter(object gate)
@@ -27,7 +61,9 @@ public void SwapTo(object gate)
2761
Monitor.Enter(gate, ref hasNewLock);
2862

2963
if (_hasLock)
64+
{
3065
Monitor.Exit(_gate);
66+
}
3167

3268
_hasLock = hasNewLock;
3369
_gate = gate;
@@ -46,3 +82,5 @@ public void Dispose()
4682
private bool _hasLock;
4783
private object? _gate;
4884
}
85+
86+
#endif

0 commit comments

Comments
 (0)