Skip to content

Conversation

@xavierjohn
Copy link
Owner

Eliminate manual Combine chains by using value objects directly in your DTOs. Validation errors are automatically collected during JSON deserialization.

Add infrastructure for automatic validation of value objects in ASP.NET Core using ITryCreatable<T> and System.Text.Json. Introduce converters, middleware, filters, and extension methods to collect and return all validation errors in standard ASP.NET Core responses. Update documentation and tests to demonstrate and verify the new pattern, enabling type-safe, boilerplate-free validation in controllers and minimal APIs.
Introduce ValueObjectValidationEndpointFilter and WithValueObjectValidation() for automatic value object validation in Minimal APIs. Endpoints now return 400 Bad Request with problem details if DTO deserialization fails. Updated README, Program.cs, and UserRoutes.cs with Minimal API setup and usage examples. Added tests for the new filter and expanded JSON serializer context for validation responses. Improved documentation for both MVC and Minimal API scenarios.
Added detailed HTTP request examples to SampleMinimalApi.http for the /users/registerWithValidation endpoint. Scenarios cover valid registration, value object validation failures (empty/invalid fields), and password validation errors. Includes comments and formatting improvements for clarity.
@github-actions
Copy link

github-actions bot commented Jan 14, 2026

Test Results

126 tests  ±0   126 ✅ ±0   0s ⏱️ ±0s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 3ab0044. ± Comparison against base commit 1af67bd.

♻️ This comment has been updated with latest results.

@codecov
Copy link

codecov bot commented Jan 14, 2026

Codecov Report

❌ Patch coverage is 92.79661% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.00%. Comparing base (1af67bd) to head (3ab0044).

Files with missing lines Patch % Lines
Asp/src/Validation/ValidatingJsonConverter.cs 81.48% 5 Missing and 5 partials ⚠️
Asp/src/Validation/PropertyNameAwareConverter.cs 90.00% 1 Missing and 1 partial ⚠️
Asp/src/Validation/ValidationErrorsContext.cs 95.91% 1 Missing and 1 partial ⚠️
.../Validation/ValueObjectValidationEndpointFilter.cs 88.88% 1 Missing ⚠️
.../src/Validation/ValueObjectValidationExtensions.cs 97.36% 0 Missing and 1 partial ⚠️
Asp/src/Validation/ValueObjectValidationFilter.cs 93.75% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #262      +/-   ##
==========================================
+ Coverage   89.65%   90.00%   +0.35%     
==========================================
  Files          72       81       +9     
  Lines        1875     2111     +236     
  Branches      373      429      +56     
==========================================
+ Hits         1681     1900     +219     
- Misses        122      131       +9     
- Partials       72       80       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Introduce Asp.Generator to discover ITryCreatable<T> types at compile time and auto-register their JSON converters for AOT scenarios. Add ValidatingConverterRegistry for fast, reflection-free lookup. Update ValidatingJsonConverterFactory to use the registry when available, falling back to reflection otherwise. Improves System.Text.Json value object validation for AOT and trimming.
- Add tests for ValidatingJsonConverter, including edge cases and struct support
- Introduce TestStructValueObject for struct converter scenarios
- Add ValidatingConverterRegistryTests for registry and factory integration, including concurrency
- Add ValueObjectValidationMiddleware and DI extension tests
- Greatly improve coverage for class/struct value objects, registry, middleware, and DI registration
- Documented AOT compatibility and setup for FunctionalDDD.Asp.Generator in main and integration ASP.NET docs
- Added detailed explanation of generator architecture, usage, and troubleshooting
- Expanded value object validation docs with diagrams and error examples
- Updated best practices and code comments for clarity and consistency
- Removed outdated AOT limitation notes; clarified generator usage for Native AOT
- Added new README for FunctionalDDD.Asp.Generator project with installation, architecture, and performance info
Remove obsolete test for non-ValidationError error collection and add ValidatingJsonConverterCoverageTests to cover additional cases, including NotFoundError/DomainError handling and GetDefaultFieldName edge cases. Add supporting test types. Bump .NET SDK to 10.0.102.
Add "Limitations" sections to README and integration-aspnet.md, clarifying that automatic validation only supports JSON content types and requires ITryCreatable<T>. Provide guidance and examples for manual validation with other formats.
Validation errors now reference actual DTO property names (not type names) during JSON deserialization.
- Added PropertyNameAwareConverter<T> to set property name context for error reporting.
- Updated ValidatingJsonConverter(s) to use property name from ValidationErrorsContext.
- Extended ValidationErrorsContext to track property name via AsyncLocal.
- Modified ValueObjectValidationExtensions to assign property-aware converters using TypeInfoResolver, supporting AOT.
- Updated sample DTOs, API routes, and HTTP requests to demonstrate correct error field mapping.
Improves error feedback accuracy for APIs and clients.
Introduce registry-based property-name-aware JSON converter wrappers to ensure validation errors are mapped to DTO property names, not type names. Add new DTOs, API route, and source generator entries for testing. Extensive tests verify correct error attribution, registry logic, and thread-safety. Update sample HTTP requests to demonstrate behavior.
Added three async tests to PropertyNameAwareConverterTests.cs to verify thread safety and error isolation in concurrent deserialization scenarios. Tests cover isolated error reporting, prevention of error leakage, and correct restoration of property names in parallel requests. Uses ConcurrentBag and Task.WhenAll to simulate real-world parallelism.
Added detailed docs and examples for property-name-aware validation, showing how error responses now use DTO property names instead of value object type names. Explained the AsyncLocal-based implementation, thread safety, AOT compatibility, and updated generator docs to reflect the new converter wrapper behavior.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces Automatic Value Object Validation for ASP.NET Core applications, eliminating the need for manual Combine chains by validating value objects directly during JSON deserialization. The feature includes an AOT-compatible source generator for Native AOT scenarios.

Changes:

  • Automatic validation infrastructure using ValidatingJsonConverter and ValidationErrorsContext
  • Source generator (Asp.Generator) for AOT-compatible converter registration
  • Middleware and filters for MVC/Minimal API integration with property-name-aware error reporting
  • Comprehensive test coverage and documentation updates

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
global.json Updated SDK version to 10.0.102
docs/docfx_project/articles/integration-aspnet.md Added comprehensive documentation for automatic validation, property-aware errors, and AOT support
Asp/src/Validation/*.cs Core validation infrastructure: middleware, filters, converters, registry
Asp/generator/*.cs Source generator for AOT-compatible converter discovery and registration
Asp/tests/Validation/*.cs Comprehensive test suite with 900+ lines of tests covering all scenarios
Examples/* Updated sample applications demonstrating the new validation feature

Comment on lines +72 to +101
foreach (var member in ns.GetMembers())
{
if (member is INamespaceSymbol childNs)
{
ScanNamespace(childNs, iTryCreatableSymbol, types);
}
else if (member is INamedTypeSymbol typeSymbol)
{
if (ImplementsITryCreatable(typeSymbol, iTryCreatableSymbol))
{
types.Add(new TypeInfo(
typeSymbol.ContainingNamespace.ToDisplayString(),
typeSymbol.Name,
typeSymbol.ToDisplayString(),
typeSymbol.IsValueType));
}

// Check nested types
foreach (var nestedType in typeSymbol.GetTypeMembers())
{
if (ImplementsITryCreatable(nestedType, iTryCreatableSymbol))
{
types.Add(new TypeInfo(
nestedType.ContainingNamespace.ToDisplayString(),
nestedType.Name,
nestedType.ToDisplayString(),
nestedType.IsValueType));
}
}
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Suggested change
foreach (var member in ns.GetMembers())
{
if (member is INamespaceSymbol childNs)
{
ScanNamespace(childNs, iTryCreatableSymbol, types);
}
else if (member is INamedTypeSymbol typeSymbol)
{
if (ImplementsITryCreatable(typeSymbol, iTryCreatableSymbol))
{
types.Add(new TypeInfo(
typeSymbol.ContainingNamespace.ToDisplayString(),
typeSymbol.Name,
typeSymbol.ToDisplayString(),
typeSymbol.IsValueType));
}
// Check nested types
foreach (var nestedType in typeSymbol.GetTypeMembers())
{
if (ImplementsITryCreatable(nestedType, iTryCreatableSymbol))
{
types.Add(new TypeInfo(
nestedType.ContainingNamespace.ToDisplayString(),
nestedType.Name,
nestedType.ToDisplayString(),
nestedType.IsValueType));
}
}
}
foreach (var childNs in ns.GetMembers().OfType<INamespaceSymbol>())
ScanNamespace(childNs, iTryCreatableSymbol, types);
foreach (var typeSymbol in ns.GetMembers()
.OfType<INamedTypeSymbol>()
.Where(type => ImplementsITryCreatable(type, iTryCreatableSymbol)))
{
types.Add(new TypeInfo(
typeSymbol.ContainingNamespace.ToDisplayString(),
typeSymbol.Name,
typeSymbol.ToDisplayString(),
typeSymbol.IsValueType));
// Check nested types
foreach (var nestedType in typeSymbol.GetTypeMembers()
.Where(nested => ImplementsITryCreatable(nested, iTryCreatableSymbol)))
{
types.Add(new TypeInfo(
nestedType.ContainingNamespace.ToDisplayString(),
nestedType.Name,
nestedType.ToDisplayString(),
nestedType.IsValueType));
}

Copilot uses AI. Check for mistakes.
Comment on lines 111 to 122
foreach (var iface in typeSymbol.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iTryCreatableSymbol))
{
// Check if T is the type itself
if (iface.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(iface.TypeArguments[0], typeSymbol))
{
return true;
}
}
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +175
foreach (var detail in fieldError.Details)
{
if (!errors.Contains(detail))
{
errors.Add(detail);
}
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
ValidationErrorsContext.AddError("outer", "Outer error");

// Act
using (var innerScope = ValidationErrorsContext.BeginScope())
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to innerScope is useless, since its value is never read.

Suggested change
using (var innerScope = ValidationErrorsContext.BeginScope())
using (ValidationErrorsContext.BeginScope())

Copilot uses AI. Check for mistakes.
Comment on lines 113 to 121
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iTryCreatableSymbol))
{
// Check if T is the type itself
if (iface.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(iface.TypeArguments[0], typeSymbol))
{
return true;
}
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iTryCreatableSymbol))
{
// Check if T is the type itself
if (iface.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(iface.TypeArguments[0], typeSymbol))
{
return true;
}
}
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, iTryCreatableSymbol) &&
iface.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(iface.TypeArguments[0], typeSymbol))
return true;

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +111
Type converterType;
if (actualType.IsValueType)
{
// Use the struct converter for value types
converterType = typeof(ValidatingStructJsonConverter<>).MakeGenericType(actualType);
}
else
{
// Use the class converter for reference types
converterType = typeof(ValidatingJsonConverter<>).MakeGenericType(actualType);
}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
Type converterType;
if (actualType.IsValueType)
{
// Use the struct converter for value types
converterType = typeof(ValidatingStructJsonConverter<>).MakeGenericType(actualType);
}
else
{
// Use the class converter for reference types
converterType = typeof(ValidatingJsonConverter<>).MakeGenericType(actualType);
}
// Use the struct converter for value types, class converter for reference types
Type converterType = actualType.IsValueType
? typeof(ValidatingStructJsonConverter<>).MakeGenericType(actualType)
: typeof(ValidatingJsonConverter<>).MakeGenericType(actualType);

Copilot uses AI. Check for mistakes.
Expand test coverage for scenarios where the source generator is not used or types are unregistered, ensuring reflection fallback works for class, struct, and nullable struct value objects. Refactor ITryCreatable<T> check logic and clean up scope management in tests. Improves reliability and error handling of the validation/converter infrastructure.
Enhance PropertyNameAwareConverterFactory to correctly handle struct value objects by wrapping them in nullable converters during reflection fallback. Expand ValueObjectValidationExtensionsTests with integration tests covering DI configuration, property name error reporting, non-object and nullable types, and reflection fallback scenarios. Add helper DTOs for testing. Ensures robust validation and error reporting with or without source generators.
Introduce a new example project showing automatic value object
validation in Minimal APIs using reflection (no Asp.Generator).
Includes endpoints, DTOs, README, launch settings, and HTTP
samples. Solution updated to include new project and generator.
Documentation compares reflection vs. AOT generator approaches.
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.

2 participants