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.

Introduce IScalarValueObject<TSelf, TPrimitive> interface and refactor all scalar value object base types (ScalarValueObject, RequiredGuid, RequiredString) to use the CRTP pattern for improved type safety and static interface support. Add ASP.NET Core integration: model binders and JSON converters for value objects, with automatic validation via AddScalarValueObjectValidation(). Update source generator, tests, and documentation to use the new pattern and enable seamless, reflection-free validation in web APIs.
Deleted ITryCreatable.cs, including the ITryCreatable<T> interface, all XML documentation, usage examples, and implementation notes. This interface previously defined a contract for validated creation of types from strings using a static TryCreate method.
- Add README section on ASP.NET Core integration and usage of IScalarValueObject for automatic model validation
- Update code samples and feature table to highlight seamless ASP.NET Core support
- Generated value objects now explicitly implement IScalarValueObject<TSelf, T>
- Add required TryCreate overloads for model binding compatibility
- Expand XML docs and examples in RequiredGuid/RequiredString to show automatic validation and error handling in controllers
- Clarify difference between manual and automatic validation approaches
- Improves developer experience by reducing boilerplate for value object validation in ASP.NET Core apps
Refactored ScalarValueObject<TSelf, T> and all derived types to explicitly implement the IScalarValueObject<TSelf, TPrimitive> interface. Updated model binder and JSON converter providers to use [DynamicallyAccessedMembers] and [UnconditionalSuppressMessage] attributes for improved trimming and AOT compatibility. Enhanced documentation and remarks regarding reflection and AOT support. Updated tests and sample classes to ensure consistent interface usage and added overloads for TryCreate methods where appropriate. This improves robustness, clarity, and future compatibility of the value object infrastructure.
Add .AddScalarValueObjectValidation() to service registration for automatic model binding validation of value objects. Introduce RegisterUserDto using value objects for registration, and add RegisterWithAutoValidation endpoint to demonstrate the feature. Update .http files with sample requests and add AUTOMATIC_VALIDATION.md documentation. Clean up namespaces in ServiceCollectionExtensions.cs. This simplifies validation, removes manual TryCreate/Combine calls, and leverages ASP.NET Core's built-in validation pipeline.
Introduce comprehensive value object validation for ASP.NET Core:
- Add ValidationErrorsContext for per-request error collection
- Replace old JSON converter with ValidatingJsonConverter & factory
- Add PropertyNameAwareConverter for property-level error reporting
- Add ValueObjectValidationMiddleware and ValueObjectValidationFilter
- Update IScalarValueObject.TryCreate to accept fieldName
- Update all value object implementations and codegen for new signature
- Revise AddScalarValueObjectValidation to wire up new infrastructure
- Improve documentation and usage examples throughout

This enables property-aware, multi-error collection during JSON deserialization and model binding, providing robust and user-friendly validation error reporting.
Added a sample HTTP request to SampleMinimalApi.http to demonstrate a successful registration using the /users/RegisterWithAutoValidation endpoint with valid user data. This helps in testing and documenting the expected behavior for successful user registration.
Introduce integration and unit tests to verify that when the same value object type (Name) is used for multiple DTO properties (FirstName, LastName), validation errors are correctly attributed to the property names, not the type name. Add RegisterWithNameDto and Name value object, update UsersController with RegisterWithSharedNameType endpoint, and expand Register.http with relevant scenarios. Includes comprehensive tests for validation context, JSON converters, and error attribution.
Introduce extension methods and endpoint filter for property-aware value object validation in Minimal APIs.
- Add AddScalarValueObjectValidationForMinimalApi and WithValueObjectValidation extensions.
- Implement ValueObjectValidationEndpointFilter for automatic validation error responses.
- Update ValidatingJsonConverter to write primitive values directly for source-gen compatibility.
- Register new endpoints and DTOs to demonstrate and test property-level validation, including scenarios with shared value object types.
- Expand HTTP test cases to verify correct error attribution to property names.
@github-actions
Copy link

github-actions bot commented Jan 21, 2026

Test Results

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

Results for commit 13c5720. ± Comparison against base commit 1af67bd.

♻️ This comment has been updated with latest results.

@codecov
Copy link

codecov bot commented Jan 21, 2026

Codecov Report

❌ Patch coverage is 92.69841% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.07%. Comparing base (1af67bd) to head (13c5720).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...p/src/ModelBinding/ScalarValueObjectModelBinder.cs 90.90% 5 Missing and 3 partials ⚠️
Asp/src/Validation/ValidatingJsonConverter.cs 83.67% 5 Missing and 3 partials ⚠️
Asp/src/Extensions/ServiceCollectionExtensions.cs 93.18% 1 Missing and 2 partials ⚠️
Asp/src/Validation/ValidationErrorsContext.cs 95.91% 0 Missing and 2 partials ⚠️
...p/src/Validation/ValidatingJsonConverterFactory.cs 87.50% 0 Missing and 1 partial ⚠️
Asp/src/Validation/ValueObjectValidationFilter.cs 95.23% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #263      +/-   ##
==========================================
+ Coverage   89.65%   90.07%   +0.42%     
==========================================
  Files          72       83      +11     
  Lines        1875     2187     +312     
  Branches      373      448      +75     
==========================================
+ Hits         1681     1970     +289     
- Misses        122      133      +11     
- Partials       72       84      +12     

☔ 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 ScalarValueObjectTypeHelper to centralize reflection logic for detecting IScalarValueObject<TSelf, TPrimitive> implementations. Refactor all usages to use this helper, improving maintainability and consistency. Add ValidationError.ToDictionary() for easier integration with ASP.NET Core validation responses, and update filters to use it. Add comprehensive unit tests for the new helper, error dictionary conversion, and validation middleware/filter behavior. Remove redundant code and streamline error handling.
Introduce AspSourceGenerator: a Roslyn source generator that auto-generates AOT-friendly System.Text.Json converters and serializer context entries for all IScalarValueObject<TSelf, TPrimitive> types. Integrate the generator as an analyzer in sample projects. Refactor reflection-based code to use a new CreateGenericInstance helper for DRYness. Add documentation and improve XML comments. This enables Native AOT support and eliminates runtime reflection for value object serialization.
- Rewrote and expanded README.md with clearer intro, feature list, quick starts, and best practices for value object validation and result conversion.
- Added REFLECTION-FALLBACK.md explaining reflection vs. source generator validation, migration, and troubleshooting.
- Introduced AddValueObjectValidation extension for unified MVC/Minimal API setup, with full XML docs and usage guidance.
- Updated .gitignore to exclude .claude files.
- Improves onboarding, clarifies AOT/reflection, and simplifies setup for all ASP.NET Core app types.
Introduce unit tests for model binding, JSON converters, service registration, and MVC validation filter related to value object infrastructure. Tests cover various value object types, error handling, registration in DI, and integration scenarios, ensuring robust and predictable behavior.
Introduces SampleMinimalApiNoAot, a new example project demonstrating FunctionalDDD.Asp usage without source generation or JsonSerializerContext. Includes comprehensive README, ready-to-use HTTP test file, and API routes for user and todo endpoints. Adds EXAMPLES-GUIDE.md comparing all examples and guiding users on when to use reflection fallback vs. AOT. Updates solution and configuration files. Emphasizes that reflection fallback is production-ready with negligible overhead, ideal for learning and most .NET apps.
Comprehensive unit and integration tests were added for the FunctionalDdd.Asp value object and validation infrastructure, covering normal, edge, and concurrency scenarios.
Null argument checks were introduced in ScalarValueObjectTypeHelper for improved robustness, and generic type instantiation now safely returns null on constraint violations.
These changes enhance reliability, error reporting, and maintainability.
ConvertToPrimitive now returns Result<T>, enabling precise parse error reporting and better ModelState messages. Updated unit tests to expect parse errors for invalid input. Added comprehensive tests for all supported primitive types and edge cases.
Added new test value objects (TestEmail, TestAge) and a variety of DTOs to ServiceCollectionExtensionsTests. Introduced comprehensive tests for JSON deserialization and validation error collection, covering multiple, mixed, nested, and nullable value object scenarios. Made TestName sealed for consistency. These changes ensure robust validation across diverse DTO structures.
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 PR introduces a new IScalarValueObject<TSelf, TPrimitive>-based model for scalar value objects and wires it into ASP.NET Core so value objects can be used directly in DTOs with automatic validation during model binding and JSON deserialization. It replaces the old ITryCreatable<T> pattern, adds a reflection-based validation pipeline plus an optional AOT-friendly source generator, and updates examples, tests, and documentation to demonstrate both MVC and Minimal API flows.

Changes:

  • Replace ITryCreatable<T> with the CRTP-based IScalarValueObject<TSelf, TPrimitive> and update ScalarValueObject, RequiredString, RequiredGuid, and EmailAddress accordingly, along with tests and docs.
  • Add a full ASP.NET Core integration layer (FunctionalDdd.Asp) for automatic value object validation, including JSON converters, model binders, middleware, filters, endpoint filters, and a Roslyn source generator for AOT scenarios.
  • Add and update sample apps (MVC, Minimal API with and without AOT) plus documentation and .http files to illustrate automatic validation, reflection fallback behavior, and usage patterns.

Reviewed changes

Copilot reviewed 90 out of 91 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
RailwayOrientedProgramming/src/ITryCreatable.cs Removes the obsolete ITryCreatable<T> interface to migrate to the new scalar value object pattern.
RailwayOrientedProgramming/src/Errors/Types/ValidationError.cs Adds ToDictionary() to project validation errors into the IDictionary<string,string[]> shape needed by ASP.NET Core validation responses.
PrimitiveValueObjects/tests/RequiredStringTests.cs Updates test value objects to inherit from RequiredString<TSelf> and adjusts TryCreate invocations to the new (string?, string?) signature.
PrimitiveValueObjects/tests/RequiredGuidTests.cs Updates the test GUID value object to use RequiredGuid<TSelf>.
PrimitiveValueObjects/tests/ITryCreatableImplementationTests.cs Reworks tests to validate TryCreate implementations and field-name behavior instead of ITryCreatable<T> interface conformance; renames the test class accordingly.
PrimitiveValueObjects/src/RequiredString.cs Converts RequiredString to RequiredString<TSelf> : ScalarValueObject<TSelf,string> with IScalarValueObject<TSelf,string> constraint, and expands XML docs and examples to cover the new validation and ASP.NET integration story.
PrimitiveValueObjects/src/RequiredGuid.cs Converts RequiredGuid to RequiredGuid<TSelf> : ScalarValueObject<TSelf,Guid> with IScalarValueObject<TSelf,Guid> constraint, and updates documentation to show the richer generated API surface and ASP.NET scenarios.
PrimitiveValueObjects/src/PrimitiveValueObjectTraceProviderBuilderExtensions.cs Updates XML docs to reference the new EmailAddress.TryCreate(string?, string?) overload.
PrimitiveValueObjects/src/ParsableJsonConverter.cs Updates documentation to refer to the new two-parameter ScalarValueObject<TSelf,T> base class.
PrimitiveValueObjects/src/EmailAddress.cs Migrates EmailAddress to ScalarValueObject<EmailAddress,string> + IScalarValueObject, wires TryCreate(string?, string?) into the new interface, and updates Parse/TryParse to delegate through the new overload.
PrimitiveValueObjects/README.md Updates examples and the core concepts table to use RequiredString<TSelf>, RequiredGuid<TSelf> and to document IScalarValueObject-based ASP.NET Core automatic validation.
FunctionalDDD.sln Adds the Asp source generator and new SampleMinimalApiNoAot project to the solution and introduces a new src solution folder.
FluentValidation/tests/ValueObject/ZipCode.cs Updates ZipCode to use ScalarValueObject<ZipCode,string> and implement IScalarValueObject<ZipCode,string>, and adjusts TryCreate signature to match the new convention.
FluentValidation/tests/ValueObject/UserId.cs Updates the test UserId to inherit from RequiredGuid<UserId>.
FluentValidation/tests/ValueObject/LastName.cs Updates the test LastName to inherit from RequiredString<LastName>.
FluentValidation/tests/ValueObject/FirstName.cs Updates the test FirstName to inherit from RequiredString<FirstName>.
Examples/Xunit/ValueObject/LastName.cs Updates the example LastName value object to inherit from RequiredString<LastName>.
Examples/Xunit/ValueObject/FirstName.cs Updates the example FirstName value object to inherit from RequiredString<FirstName>.
Examples/Xunit/ValidationExample.cs Adjusts optional value object conversion examples to use the new generic RequiredString types and the updated TryCreate signatures.
Examples/Xunit/DomainDrivenDesignSamplesTests.cs Updates example value objects to use ScalarValueObject<TSelf,T> + IScalarValueObject, adds TryCreate overloads with fieldName, and adjusts Temperature to return validation errors with the correct field name.
Examples/SampleWebApplication/src/Program.cs Enables automatic scalar value object validation by adding AddScalarValueObjectValidation() and the UseValueObjectValidation() middleware.
Examples/SampleWebApplication/src/Controllers/UsersController.cs Adds new endpoints demonstrating automatic validation via DTOs with value objects and shared-name-type scenarios, and removes a now-unnecessary pragma.
Examples/SampleWebApplication/Requests/Register.http Extends the HTTP request examples to cover automatic validation flows and shared-name-type edge cases.
Examples/SampleWebApplication/AUTOMATIC_VALIDATION.md New documentation explaining before/after controller code, how automatic validation works under the hood, and the shape of successful vs validation-error responses.
Examples/SampleUserLibrary/ValueObject/UserId.cs Updates UserId to inherit from RequiredGuid<UserId>.
Examples/SampleUserLibrary/ValueObject/Name.cs Introduces a generic Name : RequiredString<Name> value object used to test property-name attribution when the same type backs multiple properties.
Examples/SampleUserLibrary/ValueObject/LastName.cs Updates LastName to inherit from RequiredString<LastName>.
Examples/SampleUserLibrary/ValueObject/FirstName.cs Updates FirstName to inherit from RequiredString<FirstName>.
Examples/SampleUserLibrary/SampleUserLibrary.csproj Adds the Asp source generator as an analyzer reference alongside the PrimitiveValueObjects generator and Asp/PrimitiveValueObjects runtime projects.
Examples/SampleUserLibrary/Model/RegisterWithNameDto.cs Adds a DTO using the shared Name value object for both first and last name to test property-level error attribution, plus XML docs.
Examples/SampleUserLibrary/Model/RegisterUserDto.cs Adds a DTO using concrete value objects (FirstName, LastName, EmailAddress) to demonstrate automatic validation in controllers and Minimal APIs.
Examples/SampleMinimalApiNoAot/appsettings.json Standard ASP.NET Core appsettings for the new non-AOT sample Minimal API.
Examples/SampleMinimalApiNoAot/SampleMinimalApiNoAot.http HTTP examples for the non-AOT Minimal API, covering auto-validation, shared-name-type behavior, manual validation, and error responses.
Examples/SampleMinimalApiNoAot/SampleMinimalApiNoAot.csproj New Minimal API project (no AOT, no generator reference) consuming Asp and SampleUserLibrary to demonstrate reflection fallback.
Examples/SampleMinimalApiNoAot/README.md Detailed documentation of the reflection-fallback Minimal API sample, performance characteristics, and when to choose reflection vs generator.
Examples/SampleMinimalApiNoAot/Properties/launchSettings.json Launch profile for the non-AOT Minimal API sample.
Examples/SampleMinimalApiNoAot/Program.cs Configures Minimal APIs with AddScalarValueObjectValidationForMinimalApi, value object validation middleware, sample routes, and OpenTelemetry tracing.
Examples/SampleMinimalApiNoAot/API/UserRoutes.cs Defines Minimal API user routes demonstrating manual Result chaining and the new .WithValueObjectValidation()-based automatic validation endpoints.
Examples/SampleMinimalApiNoAot/API/ToDoRoutes.cs Simple TODO endpoints used in the non-AOT Minimal API sample.
Examples/SampleMinimalApi/SampleMinimalApi.http Extends the AOT Minimal API .http file with automatic validation and shared-name-type test cases and introduces a common @host variable.
Examples/SampleMinimalApi/SampleMinimalApi.csproj Adds AspSourceGenerator as an analyzer for the AOT Minimal API sample to generate AOT-safe value object converters.
Examples/SampleMinimalApi/Program.cs Integrates the generated AppJsonSerializerContext with HttpJsonOptions, adds Minimal API validation config/middleware, and includes the DTOs in the json context.
Examples/SampleMinimalApi/API/UserRoutes.cs Adds Minimal API endpoints that consume RegisterUserDto and RegisterWithNameDto directly with .WithValueObjectValidation().
Examples/EcommerceExample/ValueObjects/ProductId.cs Updates ProductId to inherit from RequiredGuid<ProductId>.
Examples/EcommerceExample/ValueObjects/OrderId.cs Updates OrderId to inherit from RequiredGuid<OrderId>.
Examples/EcommerceExample/ValueObjects/Money.cs Updates Money to ScalarValueObject<Money,decimal> + IScalarValueObject, and splits TryCreate into overloads that accept an optional field name for better error attribution.
Examples/EcommerceExample/ValueObjects/CustomerId.cs Updates CustomerId to inherit from RequiredGuid<CustomerId>.
Examples/EXAMPLES-GUIDE.md New high-level guide describing all sample projects, their trade-offs (AOT vs reflection, Minimal API vs MVC), and a recommended learning path; includes one incorrect GitHub issues link noted in review.
Examples/BankingExample/ValueObjects/TransactionId.cs Updates TransactionId to inherit from RequiredGuid<TransactionId>.
Examples/BankingExample/ValueObjects/Money.cs Updates Money to ScalarValueObject<Money,decimal> + IScalarValueObject and uses the field name in validation errors.
Examples/BankingExample/ValueObjects/CustomerId.cs Updates CustomerId to inherit from RequiredGuid<CustomerId>.
Examples/BankingExample/ValueObjects/AccountId.cs Updates AccountId to inherit from RequiredGuid<AccountId>.
DomainDrivenDesign/tests/ValueObjects/ScalarValueObjectTests.cs Updates test scalar value objects to use the new generic ScalarValueObject<TSelf,T> + IScalarValueObject pattern and adds static TryCreate implementations for use with Asp helpers.
DomainDrivenDesign/tests/ValueObjects/Money.cs Updates the test Money value object to the new ScalarValueObject<Money,decimal> + IScalarValueObject shape with a static TryCreate method.
DomainDrivenDesign/src/ScalarValueObject.cs Refactors the base scalar value object to the CRTP form ScalarValueObject<TSelf,T> with IScalarValueObject<TSelf,T> constraint and updates docs and implicit conversion accordingly.
DomainDrivenDesign/src/IScalarValueObject.cs Introduces IScalarValueObject<TSelf,TPrimitive> with a static TryCreate and Value property, forming the core contract used by validation, model binding, and converters.
DomainDrivenDesign/src/DomainDrivenDesign.csproj Adds a reference to the RailwayOrientedProgramming project to use Result and Error types in the new scalar value object abstractions.
Benchmark/BenchmarkROP.cs Updates the benchmark FirstName value object to inherit from RequiredString<FirstName>.
Asp/tests/ValueObjectValidationMiddlewareTests.cs New tests verifying that the middleware creates/disposes validation scopes correctly, handles exceptions/cancellation, and maintains isolation across requests.
Asp/tests/ValueObjectValidationFilterTests.cs New tests covering filter ordering, interaction with ModelState, clearing of pre-existing errors, case/nesting behavior, and the shape of ValidationProblemDetails.
Asp/tests/ValueObjectValidationEndpointFilterTests.cs New tests ensuring the Minimal API endpoint filter short-circuits on errors, passes through successful results, and respects concurrency/isolation.
Asp/tests/ValidationErrorsContextConcurrencyTests.cs Concurrency tests validating that ValidationErrorsContext correctly aggregates errors across many threads/scopes without duplication or leaks.
Asp/tests/ScalarValueObjectTypeHelperTests.cs Tests for the reflection helper that detects value object types, extracts primitive types, instantiates generic helpers, and handles edge cases.
Asp/tests/PropertyNameAwareConverterTests.cs Tests ensuring property names are applied to the context, restored correctly (including nested cases and exceptions), and preserved in errors.
Asp/src/Validation/ValueObjectValidationMiddleware.cs Introduces middleware that opens/closes a ValidationErrorsContext scope per request so converters can collect validation errors.
Asp/src/Validation/ValueObjectValidationFilter.cs Adds an MVC action filter that reads collected validation errors, updates ModelState, and returns a 400 ValidationProblemDetails response instead of executing the action.
Asp/src/Validation/ValueObjectValidationEndpointFilter.cs Adds a Minimal API endpoint filter that checks ValidationErrorsContext and returns Results.ValidationProblem() when errors exist.
Asp/src/Validation/ValidationErrorsContext.cs Implements the AsyncLocal-backed error collection context with scoped ErrorCollector, property-name tracking, and thread-safe aggregation into a ValidationError.
Asp/src/Validation/ValidatingJsonConverterFactory.cs Adds a JsonConverterFactory that detects IScalarValueObject implementations and creates ValidatingJsonConverter<TValueObject,TPrimitive> instances via reflection.
Asp/src/Validation/ValidatingJsonConverter.cs Implements a generic JSON converter that deserializes the primitive, calls TryCreate, and collects validation errors instead of throwing, while writing primitive values efficiently.
Asp/src/Validation/ScalarValueObjectTypeHelper.cs Provides shared reflection helpers to detect IScalarValueObject<TSelf,TPrimitive> implementations and construct generic helpers (converters/model binders).
Asp/src/Validation/PropertyNameAwareConverter.cs Wraps converters to set ValidationErrorsContext.CurrentPropertyName for per-property error attribution during deserialization.
Asp/src/ModelBinding/ScalarValueObjectModelBinderProvider.cs Introduces a model binder provider that detects scalar value objects and supplies ScalarValueObjectModelBinder<TValueObject,TPrimitive>.
Asp/src/ModelBinding/ScalarValueObjectModelBinder.cs Implements a model binder that parses primitives from standard sources and calls TryCreate, pushing validation errors into ModelState.
Asp/src/Extensions/ServiceCollectionExtensions.cs Adds extension methods to configure validation for MVC and Minimal APIs, wire up JSON options, register middleware, and expose .WithValueObjectValidation() on route handlers.
Asp/src/Asp.csproj Adds a reference to the DomainDrivenDesign project to leverage the new scalar value object abstractions.
Asp/generator/ScalarValueObjectInfo.cs Defines a simple metadata type describing discovered value objects (namespace, type name, primitive type) used by the generator.
Asp/generator/README.md Documents the Asp source generator’s purpose, usage, and benefits over reflection.
Asp/generator/AspSourceGenerator.csproj New project file for the Asp source generator, targeting netstandard2.0 and packaging as an analyzer.
Asp/docs/REFLECTION-FALLBACK.md Deep-dive documentation describing reflection fallback vs source generator behavior, performance trade-offs, and migration paths.
Asp/README.md Rewrites the Asp README to cover value object validation, result conversion, MVC/Minimal integrations, AOT vs reflection, and pointers to examples/docs.
.gitignore Ignores a new /.claude directory used by tooling.

Comment on lines +289 to +291
Found an issue or want to add an example?
- Open an issue at [github.com/anthropics/claude-code/issues](https://github.com/anthropics/claude-code/issues)
- Examples should be simple, focused, and well-documented
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

The "Contributing" section still links to github.com/anthropics/claude-code/issues, which appears unrelated to this repository and will confuse users trying to file issues. Please update this URL to point to the actual FunctionalDDD GitHub repository issues page or a more appropriate contribution channel.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +154
foreach (var property in typeInfo.Properties)
{
// Check if it's a value object (IScalarValueObject<TSelf, T>)
if (!IsScalarValueObjectProperty(property))
continue;

var propertyType = property.PropertyType;

// Create a validating converter for this value object
var innerConverter = CreateValidatingConverter(propertyType);
if (innerConverter is null)
continue;

// Wrap it with property name awareness
var wrappedConverter = CreatePropertyNameAwareConverter(innerConverter, property.Name, propertyType);
if (wrappedConverter is not null)
{
property.CustomConverter = wrappedConverter;
}
}
Copy link

Copilot AI Jan 23, 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 +56 to +62
foreach (var key in context.ModelState.Keys.ToList())
{
if (key.EndsWith("." + fieldError.FieldName, StringComparison.OrdinalIgnoreCase) ||
key.Equals(fieldError.FieldName, StringComparison.OrdinalIgnoreCase))
{
context.ModelState.Remove(key);
}
Copy link

Copilot AI Jan 23, 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 key in context.ModelState.Keys.ToList())
{
if (key.EndsWith("." + fieldError.FieldName, StringComparison.OrdinalIgnoreCase) ||
key.Equals(fieldError.FieldName, StringComparison.OrdinalIgnoreCase))
{
context.ModelState.Remove(key);
}
foreach (var key in context.ModelState.Keys
.Where(key => key.EndsWith("." + fieldError.FieldName, StringComparison.OrdinalIgnoreCase) ||
key.Equals(fieldError.FieldName, StringComparison.OrdinalIgnoreCase))
.ToList())
{
context.ModelState.Remove(key);

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 23, 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 +130 to +140
foreach (var baseType in c.BaseList.Types)
{
var typeName = baseType.Type.ToString();
if (typeName.Contains("ScalarValueObject") ||
typeName.Contains("RequiredString") ||
typeName.Contains("RequiredGuid") ||
typeName.Contains(ScalarValueObjectInterfaceName))
{
return true;
}
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Suggested change
foreach (var baseType in c.BaseList.Types)
{
var typeName = baseType.Type.ToString();
if (typeName.Contains("ScalarValueObject") ||
typeName.Contains("RequiredString") ||
typeName.Contains("RequiredGuid") ||
typeName.Contains(ScalarValueObjectInterfaceName))
{
return true;
}
}
return c.BaseList.Types
.Select(baseType => baseType.Type.ToString())
.Any(typeName =>
typeName.Contains("ScalarValueObject") ||
typeName.Contains("RequiredString") ||
typeName.Contains("RequiredGuid") ||
typeName.Contains(ScalarValueObjectInterfaceName));

Copilot uses AI. Check for mistakes.
{
for (int i = 0; i < iterations; i++)
{
var error = ValidationErrorsContext.GetValidationError();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

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

Suggested change
var error = ValidationErrorsContext.GetValidationError();
ValidationErrorsContext.GetValidationError();

Copilot uses AI. Check for mistakes.
for (int i = 0; i < iterations; i++)
{
var error = ValidationErrorsContext.GetValidationError();
var hasErrors = ValidationErrorsContext.HasErrors;
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

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

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

Suggested change
var hasErrors = ValidationErrorsContext.HasErrors;
_ = ValidationErrorsContext.HasErrors;

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +247
if (baseName == "RequiredString" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is string
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "string" in GetPrimitiveTypeName
return ("RequiredString", null);
}
}

if (baseName == "RequiredGuid" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is Guid
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "Guid" in GetPrimitiveTypeName
return ("RequiredGuid", null);
}
}

// Check for ScalarValueObject<TSelf, TPrimitive>
if (baseName == "ScalarValueObject" && baseType.IsGenericType && baseType.TypeArguments.Length == 2)
{
// Verify CRTP pattern: first type arg should be the class itself
if (SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
return ("ScalarValueObject", baseType.TypeArguments[1]);
}
Copy link

Copilot AI Jan 23, 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 (baseName == "RequiredString" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is string
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "string" in GetPrimitiveTypeName
return ("RequiredString", null);
}
}
if (baseName == "RequiredGuid" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is Guid
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "Guid" in GetPrimitiveTypeName
return ("RequiredGuid", null);
}
}
// Check for ScalarValueObject<TSelf, TPrimitive>
if (baseName == "ScalarValueObject" && baseType.IsGenericType && baseType.TypeArguments.Length == 2)
{
// Verify CRTP pattern: first type arg should be the class itself
if (SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
return ("ScalarValueObject", baseType.TypeArguments[1]);
}
if (baseName == "RequiredString" &&
baseType.IsGenericType &&
baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "string" in GetPrimitiveTypeName
return ("RequiredString", null);
}
if (baseName == "RequiredGuid" &&
baseType.IsGenericType &&
baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "Guid" in GetPrimitiveTypeName
return ("RequiredGuid", null);
}
// Check for ScalarValueObject<TSelf, TPrimitive>
if (baseName == "ScalarValueObject" &&
baseType.IsGenericType &&
baseType.TypeArguments.Length == 2 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
return ("ScalarValueObject", baseType.TypeArguments[1]);

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +237
if (baseName == "RequiredString" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is string
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "string" in GetPrimitiveTypeName
return ("RequiredString", null);
}
}

if (baseName == "RequiredGuid" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is Guid
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "Guid" in GetPrimitiveTypeName
return ("RequiredGuid", null);
}
Copy link

Copilot AI Jan 23, 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 (baseName == "RequiredString" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is string
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "string" in GetPrimitiveTypeName
return ("RequiredString", null);
}
}
if (baseName == "RequiredGuid" && baseType.IsGenericType)
{
// The type argument should be the class itself (CRTP), primitive is Guid
if (baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// Return a marker that we'll convert to "Guid" in GetPrimitiveTypeName
return ("RequiredGuid", null);
}
if (baseName == "RequiredString" &&
baseType.IsGenericType &&
baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// The type argument should be the class itself (CRTP), primitive is string
// Return a marker that we'll convert to "string" in GetPrimitiveTypeName
return ("RequiredString", null);
}
if (baseName == "RequiredGuid" &&
baseType.IsGenericType &&
baseType.TypeArguments.Length == 1 &&
SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
// The type argument should be the class itself (CRTP), primitive is Guid
// Return a marker that we'll convert to "Guid" in GetPrimitiveTypeName
return ("RequiredGuid", null);

Copilot uses AI. Check for mistakes.
Comment on lines +241 to +248
if (baseName == "ScalarValueObject" && baseType.IsGenericType && baseType.TypeArguments.Length == 2)
{
// Verify CRTP pattern: first type arg should be the class itself
if (SymbolEqualityComparer.Default.Equals(baseType.TypeArguments[0], classSymbol))
{
return ("ScalarValueObject", baseType.TypeArguments[1]);
}
}
Copy link

Copilot AI Jan 23, 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.

Copilot uses AI. Check for mistakes.
@xavierjohn xavierjohn merged commit a332385 into main Jan 24, 2026
12 checks passed
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