fix(#223): apply FluentValidation rules to every endpoint sharing a [FromQuery] DTO#224
Conversation
…FromQuery] DTO When the same [FromQuery]/[AsParameters] DTO is bound by more than one endpoint, only the first operation received its FluentValidation rules; the rest lost them (under the default RemoveUnusedQuerySchemas = true). Root cause: FluentValidationOperationFilter runs once per operation. The Issue #180 cleanup removes the temporary container schema from SchemaRepository.Schemas, but Swashbuckle keeps the type in its internal reserved-ids map, which the cleanup does not touch. For the 2nd+ endpoint, GetSchemaForType -> GenerateSchema then returns a bare $ref (no Properties) and the schema is no longer in Schemas, so the "has properties" guard is skipped and no rules are applied. Fix: in SwashbuckleSchemaProvider.GetSchemaForType, when the returned schema has no properties and the id is absent from the shared repository (the reserved-but-removed state), recover the concrete schema by generating it into a throwaway SchemaRepository. This is fully isolated - it never mutates the shared repository or its reserved-id state - so it keeps both Issue #180 cleanup and all existing OperationFilter behavior (required marking, nested [FromQuery], [FromForm]/[FromBody], Issue #216 encoding) intact. Adds a regression test (net8.0/net9.0) that drives two endpoints sharing one [FromQuery] DTO through a single SchemaRepository, as in the real Swagger pipeline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@claude review this PR |
|
Claude finished @avgalex's task in 5m 4s —— View job Reviewing PR #224
ReviewThe root-cause analysis and fix are correct. I confirmed that Swashbuckle's reserved-id/reservation state lives on the I also traced how the recovered schema is used downstream in Two things worth a look, neither of which I'd block on:
Test coverage: Note: I wasn't able to run Verdict: Sound, well-scoped fix with a solid regression test. The two notes above are follow-up-worthy, not blockers. |
…hared-dto # Conflicts: # CHANGELOG.md # version.props
|
@avgalex I see you have doubts about implementing. If the type generates a schema then point the type in a list an do the call to ReplaceSchemaId for every member on the list at he end before removing it. You could replace it with Guid.NewGuid for example |
|
@jgarciadelanoceda Thanks — that's a good pointer. The blocker for making it the fix: our net8.0/net9.0 targets pin So the plan is:
One caveat: |
Fixes #223.
Problem
When the same
[FromQuery]/[AsParameters]DTO is bound by more than one endpoint, only the first operation received its FluentValidation rules; the rest lost them. Reproduces under the defaultRemoveUnusedQuerySchemas = true. Reported and diagnosed by @jgarciadelanoceda. 🙏Root cause
FluentValidationOperationFilterruns once per operation. The Issue #180 cleanup removes the temporary[FromQuery]container schema fromSchemaRepository.Schemas, but Swashbuckle also keeps the type in its internal reserved-ids map (RegisterType/TryLookupByType), which the cleanup does not touch.For the 2nd+ endpoint,
GetSchemaForType→GenerateSchemahits the reserved-but-removed type and returns a bare$ref(noProperties); the concrete schema is no longer inSchemas, so theif (schema.Properties.Count > 0)guard is skipped and no rules are applied.That is also why
RemoveUnusedQuerySchemas = false"fixes" it — the container schema stays inSchemas(at the cost of leaking unused query schemas intocomponents).Fix
In
SwashbuckleSchemaProvider.GetSchemaForType, when the returned schema has no properties and the id is absent from the shared repository (the reserved-but-removed state), recover the concrete schema by generating it into a throwawaySchemaRepository.This is fully isolated — it never mutates the shared repository or its reserved-id state — so it keeps both the Issue #180 cleanup and all existing
OperationFilterbehavior intact:[FromQuery]parameter is wrongly marked as required #209),[FromQuery]reachability (Validator for nested type in[FromQuery]parameter is considered even when not used #211/Bring nested [FromQuery] fixes (#209 + #211) to native OpenAPI transformer and DocumentFilter #213),[FromForm]/[FromBody]request bodies,encoding.contentTypeforIFormFileparts (Add support for media types #216).It is ~10 lines in one method and changes no filter registration, so regression risk is low. (This realizes the intent of the "document filter" direction — keep the SchemaFilter, fix the shared-DTO bug — without ripping out the OperationFilter, which still owns the request-body/#216 handling that a pure replacement would have regressed.)
Tests
Adds
SharedDtoAcrossEndpointsTest(net8.0/net9.0): drives two endpoints binding one[FromQuery]DTO through a singleSchemaRepository— exactly how the real Swagger pipeline works — and asserts both parameters expose the constraints. Fails before the fix (secondName.MinLengthnull), passes after.Full suite green on all targets: net8.0 118/118, net9.0 118/118, net10.0 86/86 (OPENAPI_V2 excludes the framework-typed integration tests, as with the other OperationFilter tests).
🤖 Generated with Claude Code