Skip to content

feat: foreach-style enumerator for top-level groups (#156)#158

Merged
pedrosakuma merged 1 commit into
mainfrom
feat/group-foreach-enumerator-156
Apr 25, 2026
Merged

feat: foreach-style enumerator for top-level groups (#156)#158
pedrosakuma merged 1 commit into
mainfrom
feat/group-foreach-enumerator-156

Conversation

@pedrosakuma
Copy link
Copy Markdown
Owner

Closes #156.

Summary

Each top-level group on {Msg}DataReader now exposes a property returning a ref struct enumerator usable with foreach — zero allocation, no closure capture, ref readonly Current for zero-copy access.

if (OrderBookData.TryParse(buffer, out var reader))
{
    foreach (ref readonly var bid in reader.Bids) { /* ... */ }
    foreach (ref readonly var ask in reader.Asks) { /* ... */ }
}

Design — independent per-group views

Each property access creates a fresh enumerator that computes its own start offset by chaining static Skip{Group} helpers across prior groups. This avoids the footgun of a shared cursor:

  • ✅ Out-of-order access (Asks before Bids) works
  • ✅ Early break does not corrupt later groups
  • ✅ Repeated iteration of the same group works (each access = fresh enumerator)
  • ReadGroups continues to work unchanged (idempotent)
  • ✅ Bounds checks in MoveNext and Skip helpers — malformed buffers return false instead of throwing

Gating

Emitted only when all top-level groups are simple (no nested groups, no group-level varData). Complex messages keep ReadGroups only — the foreach API is purely additive.

Tests

6 new integration tests in GroupForeachEnumeratorTests.cs:

  • In-order iteration with multiple entries per group
  • Out-of-order access (Asks before Bids)
  • Repeated iteration produces consistent results
  • Empty groups yield nothing
  • Early break does not corrupt the next group
  • BytesConsumed is unchanged by foreach (still managed exclusively by ReadGroups)

Full suite: 187 unit + 144 integration tests green.

Each top-level group on {Msg}DataReader now exposes a property returning a
ref struct enumerator usable with foreach — zero allocation, no closure
capture, ref readonly Current for zero-copy access.

Independent per-group views: each property access creates a fresh enumerator
that computes its start offset by skipping prior groups. This means
out-of-order access, early break, and repeated iteration are all safe and
do not corrupt one another. ReadGroups remains untouched and idempotent.

Gating: emitted only when all top-level groups are simple (no nested groups,
no group-level varData). Complex messages keep ReadGroups only.

- src/SbeCodeGenerator/Generators/Types/MessageDefinition.cs:
  AppendGroupEnumerators + HasSimpleTopLevelGroupsForEnumerator gate
- 6 new integration tests covering: in-order iteration, out-of-order access,
  repeated iteration, empty groups, early break, BytesConsumed unchanged
- README + CHANGELOG updated, version 1.4.0 -> 1.5.0

Closes #156

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@pedrosakuma pedrosakuma merged commit 568c65a into main Apr 25, 2026
1 check passed
@pedrosakuma pedrosakuma deleted the feat/group-foreach-enumerator-156 branch April 25, 2026 18:47
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.

Foreach-style enumerator on {Msg}DataReader for groups (no closure alloc)

1 participant