Fix ValidationsGenerator ITypeParameterSymbol handling#65419
Conversation
…eters When endpoint handlers use generic extension methods (e.g., MapCommand<TRequest>()), the resolved method has ITypeParameterSymbol parameters. These have DeclaredAccessibility == NotApplicable, which fails the accessibility check in TryExtractValidatableType, causing the type to be silently skipped without any diagnostic. Handle ITypeParameterSymbol before the accessibility check by walking constraint types to discover validatable concrete types. Add the type parameter to visitedTypes before iterating constraints to prevent stack overflow with circular constraints. Add ContainsTypeParameter guard in ExtractValidatableMembers to prevent invalid typeof() expressions in generated code for properties whose types contain unresolved type parameters (e.g., CRTP patterns).
|
Thanks for your PR, @@ANcpLua. Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical bug in the ValidationsGenerator source generator where generic type parameters (like TRequest from a generic endpoint extension method) were either silently skipped or caused compilation errors when they leaked into generated typeof() expressions. The fix adds type parameter handling to discover validatable types through constraint types, while preventing type parameters from appearing in emitted code.
Changes:
- Added
ITypeParameterSymbolbranch inTryExtractValidatableTypeto walk constraint types and discover validatable types - Added
ContainsTypeParameterguards in both record and regular property extraction paths to prevent emittingtypeof(T)expressions - Added
ContainsTypeParameterhelper method to recursively detect type parameters in type trees
Verifies that types using the Curiously Recurring Template Pattern (CommandBase<TSelf> where TSelf : CommandBase<TSelf>) are correctly discovered and validated through the inheritance hierarchy. Covers both class and record CRTP patterns with runtime endpoint verification for Required and Range validation attributes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dotnet-policy-service agree |
|
CI failures are pre-existing infrastructure flakes, unrelated to this change:
Happy to re-run if needed. |
|
Commenter does not have sufficient privileges for PR 65419 in repo dotnet/aspnetcore |
1 similar comment
|
Commenter does not have sufficient privileges for PR 65419 in repo dotnet/aspnetcore |
|
Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime. |
Youssef1313
left a comment
There was a problem hiding this comment.
The added test seems to already be passing without the changes.
@ANcpLua Can you please rebase your branch on latest main, and ensure to create a test case that fails on main to better demonstrate the issue?
|
@copilot apply changes based on the comments in this thread |
After main commit b58523d ("Cleanup ValidationsGenerator for better incrementality") landed on 2026-04-28, `TryExtractValidatableType` no longer declares its `HashSet<ValidatableType>` and `List<ITypeSymbol>` parameters as `ref`. The new `ITypeParameterSymbol` branch added by this PR was still passing them with `ref`, so once the branch was merged with main the build failed with CS1615 on Linux x64 and Source-Build. Pass the arguments without `ref` to match the post-cleanup signature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…skip The previously added `CanValidateTypesWithGenericBaseClass` test does not differentiate between fixed and unfixed code: it uses MapPost with a concrete `CreateOrderCommand` parameter, so the generator only ever sees closed generics and the `ITypeParameterSymbol` codepath is never reached. As @Youssef1313 noted in review, that test passes on plain main without these changes. This new test makes the bug observable. It defines a generic endpoint extension `MapValidated<T>(this IEndpointRouteBuilder, string)` whose body calls `endpoints.MapPost(pattern, (T req) => ...)` — the inner MapPost is the call site the generator inspects, and the delegate parameter resolves to an *open* `ITypeParameterSymbol`. On main, the type parameter hits the `DeclaredAccessibility is not Accessibility.Public` guard in `TryExtractValidatableType` (`NotApplicable` for type parameters) and silently returns false. The concrete constraint type `UserRequest` is therefore never discovered and no `typeof(global::UserRequest)` check is emitted, even though it carries `[Required]` and `[Range]` attributes. With the fix the new `ITypeParameterSymbol` branch walks `typeParam.ConstraintTypes`, recurses into `UserRequest`, and emits the expected resolver entry. The committed snapshot is that expected output, so running the test against plain main fails with a clean snapshot diff that shows exactly the 21 missing lines for `UserRequest`. Runtime endpoint behavior is identical between fixed and unfixed code because the runtime falls back to reflection-based validation when the static resolver returns false — the snapshot is therefore the precise witness of the silent-skip bug. The runtime `VerifyEndpoint` assertions are kept as regression coverage for the validation pipeline end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@Youssef1313 thanks for the review — pushed 1. Linux x64 / Source-Build CS1615 The most recent merge from 2. Test that fails on You were right that public static RouteHandlerBuilder MapValidated<T>(this IEndpointRouteBuilder endpoints, string pattern)
where T : UserRequest
=> endpoints.MapPost(pattern, (T req) => Results.Ok());The inner + if (type == typeof(global::UserRequest))
+ {
+ validatableInfo = new GeneratedValidatableTypeInfo(
+ type: typeof(global::UserRequest),
+ members: [
+ new GeneratedValidatablePropertyInfo(
+ containingType: typeof(global::UserRequest),
+ propertyType: typeof(string),
+ name: "Name",
+ displayName: "Name"
+ ),
+ ... (Age member) ...
+ ]
+ );
+ return true;
+ }With the fix the Runtime endpoint behavior is identical fixed-vs-unfixed because the framework falls back to reflection-based validation when the static resolver returns false, so the snapshot is the only deterministic signal. The runtime Locally |
…dling Resolves the conflict introduced by upstream commit c472800 ("Add localization support to Microsoft.Extensions.Validation" (dotnet#66646)), which lands the day before this merge and rewrites the same call sites in `ValidationsGenerator.TypesParser.cs` that this PR modifies, in addition to changing the generator emitter format and the `ValidatableType`/`ValidatableProperty` model constructors. Conflict resolution in `ValidationsGenerator.TypesParser.cs`: * `ITypeParameterSymbol` branch (line 83-95) auto-merges cleanly with the new `displayAttributeSymbol`/`displayNameAttributeSymbol` lookups added at line 105-106, because the two insertions sit on opposite sides of the `DeclaredAccessibility` guard with enough unchanged context between them. * Record primary-constructor property path: `ContainsTypeParameter` early-`continue` guard is placed before the new `GetDisplayInfo` calls. The order matters: a property whose type is an unresolved type parameter (e.g. `TSelf` from `RequestBase<TSelf>`) is skipped immediately rather than computing literal/resource display info that would be discarded. * Regular property path: same ordering — guard before `GetDisplayInfo`. * `ContainsTypeParameter` helper at the end of the class is untouched by the merge. Snapshot regeneration: The emitter template in commit c472800 replaces the `displayName: "..."` constructor argument with `displayNameInfo: <DisplayNameInfo>` and emits four new file-scoped helper classes (`LiteralDisplayName`, `PropertyResourceDisplayName`, `TypeResourceDisplayName`, `DisplayAttributeCache`) into every generated resolver. The two snapshots owned by this PR (`CanValidateOpenTypeParameterReachableThroughConstraint` and `CanValidateTypesWithGenericBaseClass`) are therefore regenerated from the new emitter rather than hand-merged. The bug witness in `CanValidateOpenTypeParameterReachableThroughConstraint` is preserved: the resolver still contains `typeof(global::UserRequest)` and emits the `Name`/`Age` member entries that the constraint walk discovers — without the `ITypeParameterSymbol` branch the resolver body is empty for that type. Verification: `dotnet test src/Validation/test/Microsoft.Extensions.Validation.GeneratorTests` reports 45/45 passed locally on Debug net11.0 against the merged tree (the 15 new `DisplayName*` tests added by dotnet#66646 plus the 28 pre-existing generator tests plus the two type-parameter tests added by this PR). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@Youssef1313 rebased onto latest
Local verification on the merged tree: |
The (SEC-001) and (CRASH-001) tags carried no meaning to upstream reviewers — no other comment in src/Validation references internal classification IDs. The surrounding prose already explains the "why" (circular constraint guard, typeof(TSelf) crash). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| /// <c>Dictionary<string, T></c> — all of which would produce invalid | ||
| /// <c>typeof(...)</c> expressions in the emitted code. | ||
| /// </summary> | ||
| private static bool ContainsTypeParameter(ITypeSymbol type) |
There was a problem hiding this comment.
Is this code covered by tests?
| // where T : class, IEnumerable<T>. | ||
| visitedTypes.Add(typeSymbol); | ||
| var foundValidatable = false; | ||
| foreach (var constraintType in typeParam.ConstraintTypes) |
There was a problem hiding this comment.
Generally looks reasonable, but I'm not sure of a good use case for this. Why would you make it generic and constrained instead of having the parameter as the type you need right away?
|
The scenarios tested here also look different from what the issue original reports. |
Fixes #65418
Summary
The
ValidationsGeneratorsilently skips generic type parameters (ITypeParameterSymbol) and can emit invalidtypeof(T)expressions when type parameters leak intoValidatableProperty.Type.Changes
Source generator —
src/Validation/gen/Parsers/ValidationsGenerator.TypesParser.csITypeParameterSymbolbranch inTryExtractValidatableType— recognizes type parameters and walks theirConstraintTypesto discover concrete validatable types. Adds tovisitedTypesbefore iterating constraints to prevent infinite recursion through circular constraints (e.g.,where T : class, IEnumerable<T>).ContainsTypeParameterguards — two guards (record property path + regular property path) that skip properties whose type contains an unresolved type parameter anywhere in the type tree. Without these, the emitter would generatetypeof(TSelf)for CRTP patterns likeRequestBase<TSelf>.ContainsTypeParameterhelper — recursively checksITypeParameterSymbol,IArrayTypeSymbol, andINamedTypeSymbol.TypeArgumentsto catchT,T[],List<T>,Dictionary<string, T>,Nullable<T>, etc.Why all four blocks are one fix
ITypeParameterSymbolbranch alone (without guards) causes a regression: constraint walking discovers types whose properties contain the type parameter, leading totypeof(TSelf)in emitted code.Test plan
CanValidateOpenTypeParameterReachableThroughConstraint— repro of the silent-skip bug: the route-handler delegate parameter resolves to an openITypeParameterSymbolinside a generic endpoint extensionMapValidated<T>(...) where T : UserRequest. Onmainthe generator hits theDeclaredAccessibility is not Accessibility.Publicguard (NotApplicablefor type parameters),TryExtractValidatableTypesilently returns false, and the resolver never registersUserRequest. The committed snapshot is the expected fixed output; running the test againstmainproduces a snapshot diff showing exactly the 21 missing lines forUserRequest. (Runtime endpoint behavior is identical fixed-vs-unfixed because the framework falls back to reflection-based validation when the static resolver returns false — the snapshot is the deterministic bug witness.)CanValidateTypesWithGenericBaseClass— end-to-end regression coverage: CRTP patternCommandBase<TSelf> where TSelf : CommandBase<TSelf>with concrete derived types validates correctly (class + record paths). Base type properties ([Required]) and derived type properties ([Range]) discovered through CRTP inheritance; runtime endpoint verification confirms validation errors are produced correctly.Microsoft.Extensions.Validation.GeneratorTests, 47/47 inMicrosoft.Extensions.Validation.Tests(local).