Skip to content

Serialize a Profile's bindings from a locked snapshot, so another thread's edits can't break it [patch] - #232

Open
matt-edmondson wants to merge 1 commit into
mainfrom
fix/229-profile-json-snapshot
Open

matt-edmondson wants to merge 1 commit into
mainfrom
fix/229-profile-json-snapshot

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #229

What was wrong

Profile takes a lock on every read and write of its bindings, except one. System.Text.Json wrote them through the public [Obsolete] Chords property, which returns the live dictionary, and it enumerated that dictionary with no lock held. If another thread changed bindings during JsonSerializer.Serialize(profile), the call threw InvalidOperationException: Collection was modified.

Change

  • Chords is now [JsonIgnore].
  • A new private [JsonInclude, JsonPropertyName("Chords")] ChordsSnapshot property copies the bindings under the lock. That copy is what gets written.
  • STJ binds constructor parameters by CLR name, so the [JsonConstructor] parameter is renamed from chords to chordsSnapshot. Its type stays Dictionary<string, Chord?>?, so reading keeps the existing JsonException checks.
  • The JSON shape doesn't change: Id, Name, Description, Chords, ChordCount and BoundCommands are still written.

ChordCount and BoundCommands each still take their own lock. Within one document they can still differ from Chords by a concurrent edit. They are never read back, and the issue lists them as optional, so I left them out of this change.

Tests

New ProfileJsonConcurrencyTests:

  • Serialize_WhileAnotherThreadChangesBindings_WritesASnapshot runs the issue's reproduction. A profile with 200 bindings is serialized 2000 times while another thread loops SetChord/RemoveChord. Without the fix it fails on the first pass with "Collection was modified".
  • Serialize_KeepsTheJsonShape pins the property names and how a binding is written. It passes both before and after the change.

The existing Profile round-trip and invalid-JSON tests still pass. Full suite on Linux / .NET 10: 383/383 passed.


Generated by Claude Code

…ead's edits can't break it [patch]

System.Text.Json wrote a profile's bindings by enumerating the live Chords
dictionary with no lock, so serializing while another thread changed bindings
threw "Collection was modified". The obsolete Chords property is now
[JsonIgnore], and a private ChordsSnapshot property copies the bindings under
the lock and is written as "Chords". The JSON shape is unchanged.

Fixes #229

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vfJW499sBmEvZWVdPTbTv
@sonarqubecloud

sonarqubecloud Bot commented Nov 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants