Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Breaking changes are marked **BREAKING**.
- A collection of your own with a property that returns its items, such as `View`: `Value[0].View[0].Name` is now `Value[0][0].Name`.
- An array or `ArrayList` passed as the root object: `SyncRoot[0].Name` is now `[0].Name`.
- **BREAKING** An item that is a collection is now enumerated, so a lazy sequence in an item runs, as it does when a property holds it. An item that throws when enumerated now throws from validation, and an item that never ends makes validation hang. The same holds for a struct collection that a property holds, such as `ImmutableArray<T>`: one that throws when enumerated now throws from validation, and one that builds new objects on each read stops at the maximum depth and fails the validation. To avoid it, mark the property that holds the collection with `[SkipRecursiveValidation]`. A collection passed as the root object is now enumerated too, so a lazy sequence passed in runs during validation.
- **BREAKING** The validator now reads the properties of a struct that a property holds, when the struct has something to validate. A getter of such a struct that throws now throws from validation. To avoid it, mark the property with `[SkipRecursiveValidation]`. The properties of `GCHandle`, of the SqlTypes, such as `SqlString`, which implement `INullable`, and of `ValueTask<T>` are no longer walked, because reading them throws or waits. A null `SqlString` or a `GCHandle` that is not allocated, in a property declared as `object` or in a `List<object>`, no longer throws, and `ValueTask<T>.Result` is no longer read, so an invalid object in the result of a `ValueTask<T>` is not reported.
- **BREAKING** An error of a whole nested object now has the path of the object as its member name. This covers a class-level attribute and an `IValidatableObject.Validate` result with no member names, or with a null or empty one. Before, such an error had no member names once nested, so a caller could not tell which object failed, and a null or empty name gave the path with a dot at the end. Change any code that matches these names:
- No member names: none is now `Range`, or `Items[1]` for an item.
- A null or empty member name: `Value[0].` is now `Value[0]`.
Expand All @@ -35,15 +36,16 @@ Breaking changes are marked **BREAKING**.
- A collection that a property holds is enumerated later than before: after the objects at its own level and above are validated, not when the property is read. A lazy sequence runs later. A change that a `Validate` method makes to the collection is now seen. An item whose `Validate` adds to its own list no longer throws `Collection was modified`. When a collection throws, the exception is the same, but the result list holds different partial results.
- The NuGet package title now reads "Recursive DataAnnotations Validation". It was misspelled "Recurisive".
- The NuGet package now has a description. It showed "Package Description" before.
- Known limit: an invalid struct that an item collection returns by enumeration and also through one of its properties is reported twice, with two member names, for example `Value[0][0].Text` and `Value[0].Array[0].Text` for an `ArraySegment<T>`. The validator cannot match two copies of a struct by reference. A struct that two properties hold was already reported twice. Objects of a class are reported once.
- Known limit: an invalid struct that an item collection returns by enumeration and also through one of its properties is reported twice, with two member names, for example `Value[0][0].Text` and `Value[0].Array[0].Text` for an `ArraySegment<T>`. The validator cannot match two copies of a struct by reference. A struct that two properties hold was already reported twice. Two properties that return the same struct, such as a property and a computed copy of it, now report it twice too. Objects of a class are reported once.
- README: say what `Validator.TryValidateObject` leaves out, show the usage as C#, and tell the `Tuple` classes, which are walked, from value tuples, which are not.

### Fixed

- A struct collection that a property holds, such as `ImmutableArray<T>` or a struct of your own that implements `IEnumerable<T>`, is now validated. Before, the validator skipped every property whose type is a struct, so an invalid object inside passed, although the same struct was validated as an item of a list. The error is reported like one for a class collection, for example `Lines[0].Sku`. A model that passed before can fail now. This includes an item of a list, or the value of a dictionary, that holds such a struct. It was skipped before as having nothing to walk. A struct property that is not a collection, such as a `Money`, is still not walked.
- A struct collection that a property holds, such as `ImmutableArray<T>` or a struct of your own that implements `IEnumerable<T>`, is now validated. Before, the validator skipped every property whose type is a struct, so an invalid object inside passed, although the same struct was validated as an item of a list. The error is reported like one for a class collection, for example `Lines[0].Sku`. A model that passed before can fail now. This includes an item of a list, or the value of a dictionary, that holds such a struct. It was skipped before as having nothing to walk.
- A struct that a property holds is now walked when it has something to validate: a validation attribute on the struct or on one of its properties, `IValidatableObject`, or a property that holds an object, such as the `Value` of a `KeyValuePair<string, T>`. Before, only a struct collection was walked as a property, so `[Range]` on the `Amount` of a `Money` property, or an invalid object in a `KeyValuePair` property, passed, although the same struct was validated as an item of a list. The error names the path, for example `Price.Amount` or `Pair.Value.Name`. A model that passed before can fail now. A struct with nothing to validate, such as a `DateTime`, is still skipped, and so is a property of a struct that returns its own type, such as `DateTime.Date`. The items of a value tuple are fields, so they are still not walked.
- Objects inside an item that is itself a collection are now validated. Before, a `List<List<T>>`, a `List<HashSet<T>>`, an `object[]` that holds a list, and a list of a sealed or struct collection such as `ImmutableList<T>` passed validation even when an object inside was invalid. The error is reported with the index of each level, for example `Value[0][0].Name`. The item is still validated as an object first, so its own attributes and `IValidatableObject.Validate` run, then its items. A model that passed before can fail now.
- A collection that you pass to the validator as the root object, such as a `List<T>`, is now enumerated. Before, its items were not validated, so the list passed even when an item was invalid. The error starts with the index of the item, for example `[1].Name`. The collection is still validated as an object first, so its own attributes run. A model that passed before can fail now.
- A struct item, or a property declared as a struct, that is its default value, such as an `ImmutableArray<T>` or an `ArraySegment<T>` that nobody set, is skipped, because it holds nothing and enumerating it throws. The validator compares the memory of the struct with its default, and does not call the `Equals` of your struct, which can say "equal" for a struct that holds objects, or throw.
- A struct collection that is its default value, as an item or in a property declared as that struct, such as an `ImmutableArray<T>` or an `ArraySegment<T>` that nobody set, is skipped, because it holds nothing and enumerating it throws. The validator compares the memory of the struct with its default, and does not call the `Equals` of your struct, which can say "equal" for a struct that holds objects, or throw.

## 2.3.3 - 2026-10-01

Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The [`[SkipRecursiveValidation]`](https://github.com/tgharold/RecursiveDataAnnot
### Shared objects, cycles and computed properties

- Each object is validated once, even when several properties point to it. This also stops cycles, such as a child that points back to its parent. The error names the shortest path to the object (see "Order of results" below).
- A struct is the exception to the first rule. The validator cannot tell that two copies of a struct are the same value, so it reports an invalid struct once for each route that reaches it. For example, two properties that hold the same array of structs report each struct twice, and so does a `List<object>` that holds an `ArraySegment<T>` of structs, because the segment returns its items by enumeration and through its `Array` property. The error is not lost, but the result list has a duplicate with a different member name. For a collection type of your own, mark the property that repeats the items with `[SkipRecursiveValidation]`.
- A struct is the exception to the first rule. The validator cannot tell that two copies of a struct are the same value, so it reports an invalid struct once for each route that reaches it. For example, two properties that hold the same array of structs report each struct twice, so do two properties that return the same struct, such as a property and a computed copy of it, and so does a `List<object>` that holds an `ArraySegment<T>` of structs, because the segment returns its items by enumeration and through its `Array` property. The error is not lost, but the result list has a duplicate with a different member name. For a collection type of your own, mark the property that repeats the items with `[SkipRecursiveValidation]`.
- Objects are compared by reference. Two separate objects that are `Equals` to each other, such as records with the same values or entities with the same `Id`, are each validated.
- Only public instance properties are walked. A static property holds data of the type, not of your object, so it is not walked, as `Validator` ignores it too.
- A property that builds a new object on each read, such as `public Money Zero => new Money(0)`, could make the walk go on forever. So when an object's type overrides `Equals`, and the object equals an object of the same type, a base type or a derived type on its own path from the root, the validator checks that object's own attributes but does not walk into its properties from there. The check uses the shortest path to the object. A longer path that does not pass an equal object walks into it. An invalid object that every path reaches through an equal ancestor is not reached. This can happen when a shared object, such as a customer, is reached first through an equal ancestor.
Expand All @@ -59,20 +59,28 @@ An object that several paths reach is validated once, and its errors name the sh
### Collections

- The items of a collection that a property holds are validated. The error names the property and the index of the item: `Items[1].Name`.
- An item that is itself a collection is validated too, at each level: `Matrix[0][2].Name`. This holds for lists, arrays, sets, dictionaries and your own collection types. A dictionary is enumerated as `KeyValuePair` items, so a value that is an object is reported as `Map[0].Value.Name`. A value that is a struct collection is validated, as it is when a property holds it (see below). A value that is any other struct, such as a `Money`, is not walked, because the validator reads only properties of reference types.
- An item that is itself a collection is validated too, at each level: `Matrix[0][2].Name`. This holds for lists, arrays, sets, dictionaries and your own collection types. A dictionary is enumerated as `KeyValuePair` items, so a value that is an object is reported as `Map[0].Value.Name`. A value that is a struct is walked when it has something to validate, as it is when a property holds it (see "Structs" below).
- An item that is a collection is validated as an object first, so its own attributes and `IValidatableObject.Validate` run, then its items.
- A collection of simple values, such as `List<int>` or `string[]`, is not enumerated, because it cannot hold an invalid object.
- A collection that you pass to the validator as the root object is validated as an object, then its items are validated. The error starts with the index of the item: `[1].Name`.
- A collection that is a struct, such as `ImmutableArray<T>`, is validated when a property holds it and when it is an item of another collection: `Lines[0].Sku`. The validator walks a property whose type is a class, or a struct that is a collection of objects. A struct that is not a collection, such as a `Money`, a value tuple like `(Child, int)` or a `KeyValuePair` property, is not walked, because the validator only reads properties of reference types.
- A collection that is a struct, such as `ImmutableArray<T>`, is validated when a property holds it and when it is an item of another collection: `Lines[0].Sku`.
- The validator runs each collection it enumerates, so a lazy sequence, such as a LINQ query or an iterator, runs during validation. A sequence that never ends makes validation hang. The validator does not catch exceptions, so an exception that a collection throws when it is enumerated reaches your code. A struct collection that is its default value, such as an `ImmutableArray<T>` or an `ArraySegment<T>` that nobody set, is skipped when it is an item or when the property is declared as that struct, because it holds nothing and enumerating it throws. The validator compares the memory of the struct with its default. It does not call the `Equals` of your struct. A property declared as an interface or as `object` is always enumerated.

### Structs

- A struct that a property holds is walked when it has something to validate: a validation attribute on the struct or on one of its properties, `IValidatableObject`, or a property that holds an object, such as the `Value` of a `KeyValuePair<string, Child>`. The error names the path, as for a class: `Price.Amount`, `Pair.Value.Name`.
- A struct with nothing to validate, such as an `int`, a `Guid` or a `DateTime`, is skipped.
- A struct is validated even when it is its default value. A default `Money` with `[Range(1, 10)]` on its `Amount` fails. Only a struct collection that is its default value is skipped (see "Collections" above).
- A property of a struct that returns its own struct type, such as `DateTime.Date`, is not walked. Each read returns a new copy, so the walk would not end on its own.
- A value tuple such as `(Child, int)` keeps its items in fields, not in properties. `Validator` reads only properties, so the items of a value tuple are not walked. Use a class, a record or a `Tuple<Child, int>` instead.

### Framework types that are not walked

The validator does not walk the properties that these framework types declare: `Type` and other `MemberInfo` types, `Assembly`, `Module`, delegates, `Uri`, `FileSystemInfo`, which covers `DirectoryInfo` and `FileInfo`, `Thread`, and `Process`. Reading those properties throws or never ends, for example `Uri.Segments` on a relative `Uri`, and they carry no validation attributes.
The validator does not walk the properties that these framework types declare: `Type` and other `MemberInfo` types, `Assembly`, `Module`, delegates, `Uri`, `FileSystemInfo`, which covers `DirectoryInfo` and `FileInfo`, `Thread`, `Process`, `GCHandle`, the SqlTypes such as `SqlString`, which implement `INullable`, and `ValueTask<T>`. Reading those properties throws, waits or never ends, for example `Uri.Segments` on a relative `Uri`, or `ValueTask<T>.Result` before the task has finished, and they carry no validation attributes.

An object of one of these types is still validated. Attributes on the property that holds it, such as `[Required]` on a `Uri` property, still run. A subclass of your own, such as a class derived from `Uri`, is validated too, and the properties it adds are walked.

Other framework types are walked, so your objects inside them are validated. Examples are the `Tuple` classes, such as `Tuple<Child, int>`, the `KeyValuePair` items of a dictionary, and collections.
Other framework types are walked, so your objects inside them are validated. Examples are the `Tuple` classes, such as `Tuple<Child, int>`, `KeyValuePair`, and collections.

## Build Status

Expand Down
Loading
Loading