Skip to content

Enumerate a CompositeCommand's commands once [patch] - #110

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/undoredo-108-composite-single-enumeration
Sep 28, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/undoredo-108-composite-single-enumeration

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #108

Problem

The CompositeCommand constructor enumerated its commands argument three times:

  • once in GetAffectedItems
  • once in GetTotalSize
  • once to build _commands

This caused two bugs:

  • Single-pass sequences: a sequence that can only be read once was empty by the third pass, so the constructor threw "must contain at least one command".
  • Lazy factories: a Select factory ran three times. The metadata could then describe different command instances from the ones that Execute and Undo run.

Change

All in UndoRedo/CompositeCommand.cs:

  • One enumeration: the public constructor calls a new Materialize helper, which reads the sequence once into a list. It then chains to a private constructor that computes the affected items and size from that same list and keeps it as _commands.
  • Validation moved: Materialize does the null check and the empty check before the base constructor runs. It also rejects a null element with ArgumentException, as the triage asked. Before, a null element failed later with a NullReferenceException while the metadata was being computed.
  • GetAffectedItems and GetTotalSize are removed. The public API is unchanged.

Tests

New tests in CompositeCommandTests:

  • CompositeCommand_SinglePassSequence_KeepsEveryCommandAndItsMetadata uses a sequence that yields only on its first enumeration. It checks the commands, that they execute, the affected items and the size.
  • CompositeCommand_LazyFactory_RunsOncePerCommand checks that a counting Select factory runs exactly 3 times for 3 commands.
  • CompositeCommand_NullElement_ThrowsArgumentException

With CompositeCommand.cs reverted, all 3 new tests fail. With the fix, the full suite passes (97/97 on net10.0), and the library builds for every target.

This PR doesn't touch the files changed by the open PRs #101, #106 or #107.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M9aefrfAYJpanQVrUuFpJh


Generated by Claude Code

The constructor enumerated its commands argument three times: once for
the affected items, once for the total size and once for the command
list. A single-pass sequence was empty by the third pass and threw "must
contain at least one command", and a Select factory ran three times, so
the metadata could describe different command instances from the ones
that execute.

The public constructor now materializes the sequence once and chains to
a private constructor that computes the metadata from that list. A null
element is rejected with ArgumentException instead of failing later.

Fixes #108

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

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit a2a4dbb into main Sep 28, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/undoredo-108-composite-single-enumeration branch September 28, 2026 01:39
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