Skip to content

Update SwappableLock to support NET9+ Lock type#1077

Open
dwcullop wants to merge 1 commit intoreactivemarbles:mainfrom
dwcullop:bugfix/swappable-lock-net9
Open

Update SwappableLock to support NET9+ Lock type#1077
dwcullop wants to merge 1 commit intoreactivemarbles:mainfrom
dwcullop:bugfix/swappable-lock-net9

Conversation

@dwcullop
Copy link
Copy Markdown
Member

@dwcullop dwcullop commented Apr 10, 2026

Description:

SwappableLock is a ref struct used internally by ObservableCache to atomically swap between lock objects during write operations. It currently only supports Monitor.Enter/Monitor.Exit on object gates.

.NET 9 introduced System.Threading.Lock: a dedicated, more efficient lock type that doesn't support Monitor.Enter. This PR adds overloads so SwappableLock works with both object (pre-.NET 9) and Lock (.NET 9+).

Changes

  • CreateAndEnter(Lock gate): New factory method for .NET 9+ Lock type
  • SwapTo(Lock gate): New overload that swaps from any gate type to a Lock
  • Dispose(): Updated to release Lock if held
  • SwapTo(object gate): Updated to release Lock if previously held

Uses #if NET9_0_OR_GREATER conditional compilation. Zero behavioral change on pre-.NET 9 targets.

Why this matters

ObservableCache needs to swap locks atomically during write operations (e.g., from the notification lock to the writer lock). Without Lock support, .NET 9+ code paths that use Lock elsewhere would be incompatible with SwappableLock, forcing fallback to object locks and losing the performance benefits of the new 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>
@dwcullop dwcullop changed the title Refactor SwappableLock to support NET9+ Lock type Update SwappableLock to support NET9+ Lock type Apr 10, 2026
@dwcullop dwcullop requested review from JakenVeina and Copilot April 10, 2026 14:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds .NET 9+ System.Threading.Lock support to SwappableLock so ObservableCache can atomically swap between synchronization primitives without forcing object/Monitor locks on newer runtimes.

Changes:

  • Added CreateAndEnter(Lock gate) factory for NET9+.
  • Added SwapTo(Lock gate) overload and updated SwapTo(object gate) to correctly release whichever gate type is currently held.
  • Updated Dispose() to release a held Lock on NET9+.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


#if NET9_0_OR_GREATER
public static SwappableLock CreateAndEnter(Lock gate)
{
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On NET9+, the Lock overloads can still receive null at runtime (despite non-nullable annotations). Currently that would throw a NullReferenceException from gate.Enter()/Exit(), unlike the object overloads which throw ArgumentNullException. Consider adding an explicit null guard (e.g., ArgumentNullException.ThrowIfNull(gate)) in CreateAndEnter(Lock) to provide a consistent and clearer failure mode.

Suggested change
{
{
System.ArgumentNullException.ThrowIfNull(gate);

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +66
public void SwapTo(Lock gate)
{
if (_lockGate is null && _gate is null)
throw new InvalidOperationException("Lock is not initialized");

gate.Enter();

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SwapTo(Lock gate) should guard against a null Lock argument as well; otherwise gate.Enter() will throw NullReferenceException. Adding an explicit ArgumentNullException for a null gate keeps behavior consistent with Monitor.Enter on the object overloads and produces a more actionable exception.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +75
#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;
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds new NET9+ behavior (CreateAndEnter(Lock), SwapTo(Lock), and mixed-type swapping/release logic) but there are no tests covering it. Since the test project targets net9.0, it should be feasible to add unit tests that validate swapping object→Lock, Lock→object, and Dispose releasing the correct gate.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants