OpenAPI Schema and Examples Source Generation#44
Merged
Conversation
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Tighten scope to Minimal APIs and sync validators only, promote response-level examples and light target inference to first-class features, add bridge API extensions for example emission, and clarify diagnostics, NativeAOT constraints, and incremental-pipeline guidance. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
…nalyzer Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
There was a problem hiding this comment.
Pull request overview
This PR adds a Minimal API–focused OpenAPI source generation pipeline for Light.PortableResults.Validation, allowing synchronous validators to emit reflection-free, NativeAOT-safe endpoint OpenAPI metadata (schemas + response examples). It also introduces a hint/annotation model to document otherwise opaque validation flows and enriches response examples with representative per-error messages.
Changes:
- Added a new incremental source generator (
Light.PortableResults.Validation.OpenApi.SourceGeneration) and bundled it intoLight.PortableResults.Validation.OpenApias an analyzer asset. - Introduced generator-facing validation rule/contract/message attributes plus OpenAPI-facing opt-in + hint attributes, and added a Minimal API bridge (
ProducesPortableValidationProblemFor<TValidator>). - Extended OpenAPI response-example plumbing to support per-error example messages; updated built-in range API naming (
IsInRange/IsNotInRange) and propagated it across code/tests/docs/samples.
Reviewed changes
Copilot reviewed 62 out of 62 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/package-consumer/validate-validation-openapi-source-generation.sh | Packs local NuGet feed and validates generator works in a package consumer build. |
| tests/Light.PortableResults.Validation.Tests/ValidationErrorDefinitionTests.cs | Updates tests for IsInRange/IsNotInRange rename. |
| tests/Light.PortableResults.Validation.Tests/CheckShortCircuitCoverageTests.cs | Updates short-circuit coverage tests for range rename. |
| tests/Light.PortableResults.Validation.Tests/CheckOverloadCoverageTests.cs | Updates overload tests for range rename. |
| tests/Light.PortableResults.Validation.Tests/BuiltInRuleFamilyWorkflowTests.cs | Updates workflow tests for range rename. |
| tests/Light.PortableResults.Validation.OpenApi.Tests/Light.PortableResults.Validation.OpenApi.Tests.csproj | Adds generator project as analyzer reference for integration tests. |
| tests/Light.PortableResults.Validation.OpenApi.Tests/GeneratedValidationOpenApiIntegrationTests.cs | New end-to-end test verifying generated schemas + examples appear on OpenAPI output. |
| tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/xunit.runner.json | Enables parallel test execution for generator test project. |
| tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/PortableValidationOpenApiGeneratorTests.cs | New in-memory Roslyn tests for generator output, hints, messages, and diagnostics. |
| tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/packages.lock.json | Adds locked dependencies for new generator test project. |
| tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests/Light.PortableResults.Validation.OpenApi.SourceGeneration.Tests.csproj | New test project for generator analysis/output validation. |
| tests/Light.PortableResults.AspNetCore.OpenApi.Tests/PortableResultsOpenApiDocumentTransformerTests.cs | Adds tests asserting message-aware example behavior for both validation formats. |
| tests/Light.PortableResults.AspNetCore.OpenApi.Tests/PortableOpenApiResponseBuilderTests.cs | Adds tests for message-aware example equality and builder accumulation. |
| src/Light.PortableResults.Validation/Messaging/ValidationErrorTemplates.cs | Renames range templates to IsInRange / NotInRange. |
| src/Light.PortableResults.Validation/Definitions/ValidationRuleAttributes.cs | Adds generator-facing rule/metadata/message + error-contract attributes. |
| src/Light.PortableResults.Validation/Definitions/BuiltInValidationErrorDefinitions.Comparable.cs | Renames range definition APIs/types to InRange/NotInRange. |
| src/Light.PortableResults.Validation/Checks.Strings.cs | Annotates built-in string checks with rule/message/metadata attributes. |
| src/Light.PortableResults.Validation/Checks.Null.cs | Annotates null checks with rule/message attributes. |
| src/Light.PortableResults.Validation/Checks.Equality.cs | Annotates equality checks with typed-comparison rule/message/metadata attributes. |
| src/Light.PortableResults.Validation/Checks.Enums.cs | Annotates enum checks; adjusts nullability in Enum.IsDefined calls. |
| src/Light.PortableResults.Validation/Checks.Empty.cs | Annotates empty/not-empty checks with rule/message attributes. |
| src/Light.PortableResults.Validation/Checks.Decimals.cs | Annotates precision/scale checks with rule/metadata attributes. |
| src/Light.PortableResults.Validation/Checks.Count.cs | Annotates count checks with rule/message/metadata attributes. |
| src/Light.PortableResults.Validation/Checks.Comparable.cs | Renames IsInBetween→IsInRange, adds rule/message/metadata annotations for comparisons/ranges. |
| src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiRouteHandlerBuilderExtensions.cs | Adds ProducesPortableValidationProblemFor<TValidator> Minimal API bridge. |
| src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiExampleMetadataAttribute.cs | Adds attribute for constant example metadata values. |
| src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiExampleHintAttribute.cs | Adds attribute for example hints (code/target/message). |
| src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiErrorMetadataPropertyAttribute.cs | Adds attribute for inline metadata schema properties. |
| src/Light.PortableResults.Validation.OpenApi/PortableValidationOpenApiErrorHintAttribute.cs | Adds attribute for explicit error code + optional metadata type hints. |
| src/Light.PortableResults.Validation.OpenApi/Light.PortableResults.Validation.OpenApi.csproj | Bundles generator as analyzer asset into OpenAPI package. |
| src/Light.PortableResults.Validation.OpenApi/IPortableValidationOpenApiContract.cs | Adds static-abstract contract interface for generated validator metadata. |
| src/Light.PortableResults.Validation.OpenApi/GeneratePortableValidationOpenApiAttribute.cs | Adds opt-in attribute with AllowUnknownErrorCodes. |
| src/Light.PortableResults.Validation.OpenApi/BuiltInValidationErrorBuilderExtensions.cs | Extends typed helper APIs to optionally add example entries. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/ValidatorOpenApiModels.cs | Adds analysis DTOs and equality/comparer helpers for generator pipeline. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/ValidatorOpenApiEmitter.cs | Emits deterministic generated partial validator contract source. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/PortableValidationOpenApiGenerator.cs | Implements incremental generator adapter with ForAttributeWithMetadataName. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/packages.lock.json | Adds locked dependencies for generator project. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/Light.PortableResults.Validation.OpenApi.SourceGeneration.csproj | New netstandard2.0 generator project definition. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/KnownTypeNames.cs | Centralizes metadata-name constants for generator symbol resolution. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/DiagnosticDescriptors.cs | Adds LPRSG diagnostics for unsupported shapes, hints, and message templates. |
| src/Light.PortableResults.Validation.OpenApi.SourceGeneration/CodeWriter.cs | Adds simple indentation-aware code writer for emitted source. |
| src/Light.PortableResults.AspNetCore.OpenApi/Schemas/PortableResultsOpenApiSchemas.cs | Adds example value for ErrorCategory schema. |
| src/Light.PortableResults.AspNetCore.OpenApi/PortableValidationProblemOpenApiBuilder.cs | Adds message-aware WithErrorExample builder API. |
| src/Light.PortableResults.AspNetCore.OpenApi/PortableProblemOpenApiBuilder.cs | Adds message-aware WithErrorExample builder API. |
| src/Light.PortableResults.AspNetCore.OpenApi/PortableOpenApiErrorResponseAttributeBase.cs | Adds ErrorExamples storage to route metadata attribute base. |
| src/Light.PortableResults.AspNetCore.OpenApi/PortableOpenApiErrorExampleEntry.cs | Introduces example-entry model with message + metadata and equality/hash. |
| src/Light.PortableResults.AspNetCore.OpenApi/PortableOpenApiBuilderUtilities.cs | Adds append helper for accumulating example entries. |
| src/Light.PortableResults.AspNetCore.OpenApi/Generation/PortableResultsOpenApiDocumentTransformer.cs | Composes configured error examples into OpenAPI response examples (rich + ASP.NET compatible). |
| samples/NativeAotMovieRating/NewMovieRating/NewMovieRatingValidator.cs | Opts validator into generator and updates range call. |
| samples/NativeAotMovieRating/NewMovieRating/NewMovieRatingEndpoint.cs | Switches endpoint to ProducesPortableValidationProblemFor<...> using generated contract. |
| samples/NativeAotMovieRating/NativeAotMovieRating.csproj | Adds generator project as analyzer reference for sample. |
| samples/NativeAotMovieRating/GetMovies/GetMoviesEndpoint.cs | Updates range call to IsInRange. |
| README.md | Documents the generator, hints, example metadata/messages, and limitations; updates range naming. |
| Light.PortableResults.slnx | Adds generator project and new test project; adds AI plan docs. |
| Directory.Packages.props | Centrally pins Roslyn analyzer/generator package versions. |
| benchmarks/Benchmarks/packages.lock.json | Updates lock file to reflect central Roslyn package versions and package version bumps. |
| benchmarks/Benchmarks/FlatDtoValidationBenchmarks.cs | Updates range call to IsInRange. |
| ai-plans/0043-3-plan-deviations.md | Adds plan deviation documentation for generator + hints + messages. |
| ai-plans/0043-2-openapi-example-messages.md | Adds plan doc for example messages feature. |
| ai-plans/0043-1-improve-documentation-hints.md | Adds plan doc for richer hint model. |
| ai-plans/0043-0-openapi-source-generation.md | Adds original plan doc for validator-driven OpenAPI source generation. |
Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
…a order Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
…orrectly by error category Signed-off-by: Kenny Pflug <kenny.pflug@live.de>
Minimum allowed line rate is |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #43
Summary
The original direction stayed intact: validation OpenAPI metadata is generated from synchronous validators, emitted through a static contract, and consumed by Minimal API endpoint metadata without reflection. The implementation added two follow-up capabilities that were not fully covered by
0043-0: explicit documentation hints for opaque validation paths, and representative per-error example messages.Most boundaries from the original plan still hold. The generator remains Minimal API-focused, supports direct
Validator<T>andValidator<TSource, TValidated>inheritance only, skips nested control-flow checks with diagnostics, treatsMust(...),Custom(...), andErrorOverridesas opaque unless documented explicitly, and keeps generated code NativeAOT-safe.Runtime API Deviations
The generated contract target is the public static-abstract interface
IPortableValidationOpenApiContractinLight.PortableResults.Validation.OpenApi. Generated validators implement this interface and exposeConfigurePortableValidationOpenApi(PortableValidationProblemOpenApiBuilder builder), matching the original reflection-free design.The Minimal API bridge is implemented as
ProducesPortableValidationProblemFor<TValidator>(...)inLight.PortableResults.Validation.OpenApi. It delegates to the existing ASP.NET Core OpenAPI builder, first applying the generated validator contract and then invoking the caller'sconfigurecallback. This preserves the intended ordering: generated metadata is the baseline, endpoint-local manual configuration can override or extend it.PortableOpenApiErrorExampleEntryandWithErrorExample(...)were extended beyond the original plan to include a nullable per-errormessagebefore metadata. The OpenAPI transformer now uses that message for both rich validation problem examples and ASP.NET Core-compatible validation problem examples, falling back to"Validation failed."when no message is supplied.Hint Model Deviations
The first plan treated explicit emitted-error hints as an escape hatch but did not fully specify their public API. The implementation made this a first-class feature:
PortableValidationOpenApiErrorHintAttributedocuments a known error code and can optionally point to a metadata type.PortableValidationOpenApiErrorMetadataPropertyAttributedocuments inline metadata schema properties when a dedicated metadata type would be unnecessary or awkward.PortableValidationOpenApiExampleHintAttributedocuments response-example entries for opaque paths and can includeTargetandMessage.PortableValidationOpenApiExampleMetadataAttributesupplies compile-time constant metadata values for matching example hints.Hints can be placed on the validator class or directly on
PerformValidation. They compose with inferred rules, are deduplicated when compatible, and produce diagnostics when schema contracts conflict. Example-only hints also document the code as a code-only schema entry, avoiding a redundant error hint for common opaque paths.AllowUnknownErrorCodesis available onGeneratePortableValidationOpenApiAttributeas an explicit opt-in. Hints do not imply it. This is a sharper separation than the original plan text: hints document known contracts, whileAllowUnknownErrorCodeskeeps the response schema non-exhaustive for additional codes that are not enumerable at build time.Message Deviations
The implementation added
ValidationRuleMessageAttributeinLight.PortableResults.Validation.Definitions. Built-in validation check methods now carry compile-time message templates where the default framework message can be represented statically.The generator substitutes
{displayName}and metadata placeholders such as{minLength},{maxLength},{lowerBoundary},{upperBoundary}, and{comparativeValue}when all required values are known at compile time. The display name is resolved from an explicit constantdisplayNameargument toValidationContext.Check(...), then from the inferred JSON-style target, and otherwise omitted.If a template is valid but a value is not statically known, the generator still emits the schema and example entry but leaves the message as
null, letting the transformer use the fallback message. Invalid templates produce warning diagnostics:LPRSG0013for unknown placeholders.LPRSG0014for malformed brace sequences or unsupported placeholder syntax.This is a deliberate documentation feature only. Generated messages are representative defaults and are not intended to exactly model runtime customization, localization, culture-specific formatting, display-name customization, or
ErrorOverrides.Message.Analysis and Generation Deviations
The source generator is implemented as an
IIncrementalGeneratorusingForAttributeWithMetadataName(...), as planned. The thin generator adapter calls the publicValidatorOpenApiAnalyzer.Analyze(...)method, which returns aValidatorOpenApiAnalysiscontaining diagnostics, hint name, and source. The implementation therefore made the analysis directly testable without a full generator-driver flow.The generator does not reference runtime projects as normal assembly references. It resolves contracts by metadata name through
KnownTypeNames, while generated source depends on the runtime packages in the consuming application.Generated source uses a deterministic controlled using block and emits builder calls rather than runtime reflection. It now emits:
WithErrorCodes(...)calls for registered no-metadata or globally registered contracts;WithInRangeError<T>()for comparison and range contracts;WithErrorMetadata(...)calls for annotated custom rules and inline hint metadata;WithErrorExample(...)calls containing code, target, optional representative message, and constant metadata values where available;AllowUnknownErrorCodes()only when explicitly requested.The implementation also chose direct in-memory Roslyn generator tests instead of introducing snapshot infrastructure such as
Verify.SourceGenerators. This still covers emitted source, diagnostics, and generated OpenAPI document behavior while keeping the test stack smaller.Validation Rule Annotation Deviations
The core validation layer gained source-generator-facing annotations in
Light.PortableResults.Validation.Definitions:ValidationRuleAttributeValidationRuleMetadataAttributeValidationRuleMessageAttributeValidationErrorContractAttributeValidationErrorMetadataContractAttributeThese remain OpenAPI-agnostic and use source-generator-friendly attribute shapes only. Built-in checks were annotated rather than hard-coded into the generator. The comparative range rename from
IsInBetween/IsNotInBetweentoIsInRange/IsNotInRangewas applied, aligning method names withValidationErrorCodes.InRangeandValidationErrorCodes.NotInRange.One practical addition beyond the first plan is inline metadata schema generation for user-defined rules and hints. When an annotated rule references a validation error contract, the generator can emit an endpoint-local schema with
PortableOpenApiSchemaTypeMapper.Map<T>()instead of requiring a pre-registered metadata type for every custom case.Documentation, Sample, and Tests
The
NativeAotMovieRatingsample now uses[GeneratePortableValidationOpenApi]onNewMovieRatingValidatorandProducesPortableValidationProblemFor<NewMovieRatingValidator>(...)on the endpoint. Manual validation error-code configuration was removed from the sample endpoint, with endpoint-local format configuration kept in the callback.The README now documents the generator opt-in model, supported validator shapes, top-level-only analysis boundary, explicit hints, example metadata, representative messages, and the distinction between hints and
AllowUnknownErrorCodes().Tests were added across the source-generation and OpenAPI projects for generator output, diagnostics, custom annotated rules, hints, example metadata, messages, builder behavior, document transformation, and generated OpenAPI integration. The package layout also bundles the generator into
Light.PortableResults.Validation.OpenApias an analyzer asset underanalyzers/dotnet/cs, matching the original distribution intent.Remaining Boundaries
The following limitations remain intentional after the deviations:
PerformValidation, and runtime-customized validation messages remain outside the generator's static analysis scope.