Skip to content

Validate a struct collection that a property holds - #84

Merged
tgharold merged 8 commits into
masterfrom
tgh-20261002-struct-collection-property
Oct 2, 2026
Merged

tgharold merged 8 commits into
masterfrom
tgh-20261002-struct-collection-property

Conversation

@tgharold

@tgharold tgharold commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Motivation

The validator reads only the properties of reference types, so a property whose type is a struct was skipped. That included a struct collection such as ImmutableArray. The same struct was validated when it was an item of a list, since the nested collections change in #82. A model could therefore pass validation only because the collection sat one level higher, which is a false pass of the kind the 3.0 series is removing.

An independent review of everything since v2.3.3 found two bugs in the first version of this change. Both are fixed here, with specs that failed before the fix.

Summary

  • A property of a struct type is walked when the struct is a collection of objects that can have attributes, and for a Nullable of such a struct. The path is the same as for a class collection, for example Lines[0].Sku. A struct that is not a collection, such as a Money, is still not walked.
  • A struct collection that is its default value, such as an ImmutableArray nobody set, is skipped, as an item already was. The check now compares memory with RuntimeHelpers.Equals and no longer calls the Equals of the caller's struct. Before, a struct that compared only an Id passed while it held invalid objects, and an Equals that threw ended validation.
  • The default skip applies to a property only when the property is declared as a struct. A property declared as an interface or object was always enumerated, and still is. This closes a false pass that the first version introduced.
  • The branch includes a merge of master, so Add a maximum depth of 128 levels #83 (maximum depth) applies. A struct collection property is counted like any other collection, and a computed one now stops at the limit and fails the validation.
  • README and CHANGELOG are updated. The "now throws when enumerated" and "never ends" cases are folded into the existing BREAKING enumeration entry. Stale comments and version labels (2.4.0 now 3.0, four checks now five) are fixed.

Not in this PR: the depth is still counted along the walk's route, so a valid graph with many back-references can fail at 128. That is a design problem in #83, and it gets its own PR (a breadth-first walk). KeyValuePair and tuple properties are also still not walked.

Test plan

  • The first commit unskips 8 specs and adds specs that failed until the fix. The second test commit adds 12 more that failed until the fix to the default check.
  • Guards for a struct collection against the maximum depth, for a dictionary value that is a struct collection, and for IsWalked and IsLeafType.
  • Full suite on net8.0 and net10.0: 551 passed, 6 skipped, 0 failed.
  • The net481 test project builds with -p:IncludeNetFramework=true.
  • Windows CI runs the net481 tests.

tgharold and others added 8 commits October 2, 2026 08:14
Unskip the 8 specs, and add specs for an object shared by two struct
collections and for [SkipRecursiveValidation]. They fail until the fix
is added. The guards that pin the old gap still pass.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The validator skipped every property of a struct type, so an invalid
object inside an ImmutableArray property passed, while the same struct
was validated as an item of a list. A property of a struct that is a
collection of items that can have attributes is now enumerated. A
default struct is skipped, as an item is.

Delete the two guards that pinned the gap.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Specs for a struct collection whose Equals says "equal to default"
while it holds invalid objects, or throws. The first group fails
until IsDefaultStruct stops calling the Equals of the caller's type.
Guards for a struct collection property against the maximum depth,
for a dictionary value that is a struct collection, and for IsWalked
and IsLeafType.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
IsDefaultStruct called the Equals of the caller's struct. A struct
that compares only an Id, or has no fields, then counted as default
while it held invalid objects, so the validation passed. An Equals
that throws ended the validation. Compare the memory with
RuntimeHelpers.Equals instead.

Skip a default struct in a property only when the property is
declared as a struct. A property declared as an interface or object
was always enumerated, and a boxed struct in it is enumerated again.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Say "five checks" where five are listed, use 3.0 for the next
release, and update the comments that said a struct collection
property is not read.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@tgharold
tgharold merged commit 430315f into master Oct 2, 2026
3 checks passed
@tgharold
tgharold deleted the tgh-20261002-struct-collection-property branch October 2, 2026 12:54
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.

1 participant