Skip to content

Walk struct properties that have something to validate - #90

Merged
tgharold merged 3 commits into
masterfrom
tgh-20261002-walk-struct-properties
Oct 2, 2026
Merged

tgharold merged 3 commits into
masterfrom
tgh-20261002-walk-struct-properties

Conversation

@tgharold

@tgharold tgharold commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Motivation

A struct in a property was never walked unless it was a collection. So [Range] on the Amount of a Money property, a positional record struct with [property: Range], an invalid object in a KeyValuePair<string, T> property, and a struct that wraps a class all passed validation. The same struct as an item of a list was validated. This closes those false passes for 3.0, which already carries breaking changes. Walking struct properties means reading getters the validator never read before, so the PR also denies the framework structs whose getters throw or wait.

Summary

  • A struct property is walked when the struct is not a leaf type: it has a validation attribute, implements IValidatableObject, has a walked property such as KeyValuePair.Value, or is a collection of objects. A struct with nothing to validate, such as int, Guid or DateTime, is still skipped.
  • A struct property that returns its own struct type, such as DateTime.Date, is not walked. Each read is a new copy that the walk can't match by reference, and without the rule DateTime would stop being a leaf type.
  • The framework deny list adds GCHandle, INullable (all SqlTypes) and ValueTask<T>, matched by name because netstandard2.0 can't name it. A null SqlString or an unallocated GCHandle in an object property or a List<object> no longer throws. ValueTask<T>.Result is no longer read, even in an object property.
  • Value tuple items are fields, so they are still not walked. The README says so.
  • README gets a Structs section. The changelog gets a Fixed entry, a BREAKING Changed entry for the newly read getters and the deny list, and the new duplicate route in the struct known limit.

Probe of all 510 public runtime structs: under this rule 138 are newly walkable. 20 still throw on a default value: struct enumerators, async method builders, metadata-reader structs, DependentHandle, GCHandle<T> and PinnedGCHandle<T>. They are not model types and were already walked in an object property, so they are left off the deny list.

Test plan

  • Specs first: 20 new specs fail against master, and the new guards pass there
  • With the fix: 610 passed, 7 skipped on net8.0 and net10.0
  • Test project builds for net481 with -p:IncludeNetFramework=true
  • UpgradeCompatibilityTests still holds only shapes where 3.0 matches 2.2. The struct property moved to OddShapeTests.StructProperties
  • Every new learn.microsoft.com link returns 200
  • CI passes, including net481 on Windows

tgharold and others added 3 commits October 2, 2026 17:25
A struct property is to be walked when the struct has something to
validate, as a struct item already is. Specs: an attribute inside a
struct, a nullable and a record struct, a default struct, a
KeyValuePair property and a struct that holds an object. The
linked-list item gains a route through LinkedListNode.Value.

Guards: a leaf struct such as DateTime is not walked, a property of
its own struct type is not walked, value tuple items (fields) are not.

Specs for the deny list: a null SqlString, SqlDecimal or SqlBytes, a
GCHandle that is not allocated and ValueTask<T>.Result are not read,
in an object property, a list item or a typed property.

The upgrade test no longer holds a struct property, because 3.0
differs from 2.2 there.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A struct property is now walked when the struct is not a leaf type:
it has a validation attribute, implements IValidatableObject, has a
walked property such as KeyValuePair.Value, or is a collection of
objects. Before, only a struct collection was walked, so an invalid
struct in a property passed while the same struct as an item failed.

A property of a struct that returns the struct's own type, such as
DateTime.Date, is not walked. Each read is a new copy, and without
this DateTime would stop being a leaf type.

Deny GCHandle, INullable (the SqlTypes) and ValueTask<T>, whose
getters throw or wait. ValueTask<T> is matched by name, because
netstandard2.0 cannot name it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add a Structs section to the README: which struct properties are
walked, that a default struct is validated, that a property of the
struct's own type is not walked, and that value tuple items are
fields. Add GCHandle, the SqlTypes and ValueTask<T> to the framework
types that are not walked.

Changelog: a Fixed entry for struct properties, a BREAKING entry for
the getters that are now read and the deny list, and the new
duplicate route in the struct known limit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@tgharold
tgharold merged commit c8dba54 into master Oct 2, 2026
3 checks passed
@tgharold
tgharold deleted the tgh-20261002-walk-struct-properties branch October 2, 2026 21:48
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