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
17 changes: 9 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ Breaking changes are marked **BREAKING**.

- **BREAKING** A maximum depth of 128 levels. An object at a path of more than 128 segments is not validated, and the validation fails with one error at that path: `The object is nested more than 128 levels deep and was not validated.` Each property step and each collection index counts as one level, so a tree that holds its children in a `List<T>` can be 64 levels deep. The depth of an object is the length of the shortest path to it, so a link back to a parent does not make it deeper. Before, the validator had no limit, and a graph of about 1,500 objects, or a property that builds a new object on each read, overflowed the stack, which ends the process. The validator now keeps its work in a queue, so the depth of a graph no longer uses the stack. A graph that is deeper than 128 levels passed before and fails now. Change any model that is that deep, or mark the property that leads into it with `[SkipRecursiveValidation]`. The limit is not a setting.

### 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.
- 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`. A model that passed before can fail now.
- An item that is a collection is still validated as an object first, so its own attributes and `IValidatableObject.Validate` run, as before. Its items are then validated as well.
- 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.

### Changed

- **BREAKING** The member names of an object in an item that is a collection. The validator now enumerates the items first, so the path no longer goes through a property of the item. Change any code that matches these names:
Expand All @@ -42,7 +34,16 @@ Breaking changes are marked **BREAKING**.
- An object that `Equals` an ancestor on its shortest path is still walked when a longer path reaches it without an equal ancestor. Before, the first path in property order decided, so a model passed or failed by the order of its properties. Known limit: an object that every path reaches through an equal ancestor is not walked. This happens when a shared object, such as a customer, is reached first through the equal ancestor.
- 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.
- 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.
- 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.

## 2.3.3 - 2026-10-01

Expand Down
16 changes: 9 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ dotnet test test/RecursiveDataAnnotationsValidation.Tests/RecursiveDataAnnotatio

## Key Files to Understand

- `RecursiveDataAnnotationValidator.cs` - Main implementation that handles recursive object validation
- `RecursiveDataAnnotationValidator.cs` - Main implementation. The nested class `GraphWalk` walks the object graph.
- `Extensions/TypeExtensions.cs` - Decides which types and properties the walk reads (`IsLeafType`, `GetWalkedProperties`)
- `SkipRecursiveValidation.cs` - Attribute for excluding properties from recursive validation
- Test models in `test/RecursiveDataAnnotationsValidation.Tests/TestModels/` show various usage patterns

Expand All @@ -62,13 +63,14 @@ dotnet test test/RecursiveDataAnnotationsValidation.Tests/RecursiveDataAnnotatio

## Development Notes

The recursive validator handles:
1. Cyclical object references to prevent infinite loops
2. Collections (IEnumerable) with recursive validation of items
3. Null value handling for nested objects and collections
4. Proper error message formatting that includes property paths
The validator walks the graph breadth first, with one queue and no recursion. It reads public instance properties by reflection.

The validator uses reflection to examine object properties and recursively validate nested objects.
1. Each object is validated once, compared by reference, so cycles and shared objects stop. The error names the shortest path to the object.
2. Collections (`IEnumerable`) are enumerated, and each item is validated. An item that is a collection is enumerated too.
3. The maximum depth is 128 levels. Each property and each collection index is one level. A deeper object fails the validation with one error.
4. Member names are paths from the root, such as `Lines[1].Quantity`.

The README describes the behavior that callers see. Keep it in step with the code.

## Changelog

Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
MIT License

Copyright (c) 2016-2018 Mike Reust
Copyright (c) 2020 Thomas Harold
Copyright (c) 2020-2026 Thomas Harold

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# RecursiveDataAnnotationsValidation

Allows recursive validation of sub-objects in a class when using [DataAnnotations validation](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-3.1) (also known as Attribute Validation). The current version of .NET Core's attribute validation does not handle objects within objects (or collections of objects). Therefore it is necessary to add some glue code to recurse through the object graph.
Validates a whole object graph with [DataAnnotations](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations) attributes. `Validator.TryValidateObject` checks only the properties of the object you pass it. It does not validate the objects that those properties hold, or the items of a collection. This library walks the graph and validates each object it finds.

## Installation

### .NET Core
### .NET CLI

$ dotnet add package RecursiveDataAnnotationsValidation

Expand All @@ -16,10 +16,12 @@ Allows recursive validation of sub-objects in a class when using [DataAnnotation

Usage of the recursive validation is nearly identical to using the standard validator.

var validator = new RecursiveDataAnnotationValidator();
var validationResults = new List<ValidationResult>();
var result = validator.TryValidateObjectRecursive(sut, validationResults);

```csharp
var validator = new RecursiveDataAnnotationValidator();
var validationResults = new List<ValidationResult>();
var isValid = validator.TryValidateObjectRecursive(model, validationResults);
```

There are more examples in the [example](https://github.com/tgharold/RecursiveDataAnnotationsValidation/tree/master/examples) and [test](https://github.com/tgharold/RecursiveDataAnnotationsValidation/tree/master/test) projects.

### Member names
Expand Down Expand Up @@ -61,7 +63,7 @@ An object that several paths reach is validated once, and its errors name the sh
- 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 tuple or a `KeyValuePair`, 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 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.
- 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.

### Framework types that are not walked
Expand All @@ -70,7 +72,7 @@ The validator does not walk the properties that these framework types declare: `

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 tuples, `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>`, the `KeyValuePair` items of a dictionary, and collections.

## Build Status

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<Title>Recursive DataAnnotations Validation</Title>
<Description>Validates a whole object graph with DataAnnotations attributes, including nested objects and the items of collections. Each object is validated once, so cycles stop. Use [SkipRecursiveValidation] to leave out a property.</Description>
<Authors>Thomas Harold</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/tgharold/RecursiveDataAnnotationsValidation</RepositoryUrl>
Expand Down
Loading