What's wrong
Keybinding/Models/Profile.cs:113 returns a wrapper over the profile's internal dictionary instead of a copy:
public IReadOnlyDictionary<string, Chord> GetAllChords() => Chords.AsReadOnly();
Profile.cs:145 has the same problem:
public IReadOnlyCollection<string> BoundCommands => Chords.Keys;
KeybindingService.GetAllChords() and GetAllChords(profileId) pass this wrapper straight through. The other collection getters (GetAllProfiles, GetAllCommands, …) return ToList() snapshots, so this one behaves differently from the rest of the API.
Failure scenarios (single-threaded, so separate from #109)
- The returned value changes after it is returned:
var all = svc.GetAllChords(); // 1 binding
svc.BindChord("b", chord);
all.Count; // 2, expected 1
- Binding while enumerating throws:
foreach (var kv in svc.GetAllChords())
if (!svc.HasChordBinding("b")) svc.BindChord("b", kv.Value);
// InvalidOperationException: Collection was modified; enumeration operation may not execute.
Both were reproduced with a temporary MSTest test against the current main.
Code that snapshots bindings for a settings UI, or that copies bindings between profiles, hits this directly. Because the return type is read-only, callers reasonably assume they got a snapshot.
Suggested fix
public IReadOnlyDictionary<string, Chord> GetAllChords() => new Dictionary<string, Chord>(Chords).AsReadOnly();
public IReadOnlyCollection<string> BoundCommands => [.. Chords.Keys];
Acceptance: a dictionary returned by GetAllChords() doesn't change after a later BindChord or UnbindChord, and binding while enumerating it doesn't throw. Add tests for both.
What's wrong
Keybinding/Models/Profile.cs:113returns a wrapper over the profile's internal dictionary instead of a copy:Profile.cs:145has the same problem:KeybindingService.GetAllChords()andGetAllChords(profileId)pass this wrapper straight through. The other collection getters (GetAllProfiles,GetAllCommands, …) returnToList()snapshots, so this one behaves differently from the rest of the API.Failure scenarios (single-threaded, so separate from #109)
Both were reproduced with a temporary MSTest test against the current
main.Code that snapshots bindings for a settings UI, or that copies bindings between profiles, hits this directly. Because the return type is read-only, callers reasonably assume they got a snapshot.
Suggested fix
Acceptance: a dictionary returned by
GetAllChords()doesn't change after a laterBindChordorUnbindChord, and binding while enumerating it doesn't throw. Add tests for both.