Skip to content

GetAllChords() returns a live view, so it changes under the caller and throws "Collection was modified" when binding while iterating #127

Description

@matt-edmondson

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)

  1. The returned value changes after it is returned:
    var all = svc.GetAllChords();   // 1 binding
    svc.BindChord("b", chord);
    all.Count;                      // 2, expected 1
  2. 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.

Activity

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions