Conversation
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds documentation-only OpenAPI support for Light.PortableResults by introducing schema-only CLR types and endpoint/attribute helpers, plus integrating OpenAPI generation into the NativeAotMovieRating sample.
Changes:
- Renames the success OpenAPI schema/helper surface to
PortableSuccessResponse/ProducesPortableSuccessResponse(...)(+ MVC attribute). - Adds schema-only problem-details types and Minimal API/MVC helpers for documenting failure responses (problem + validation formats).
- Integrates
Microsoft.AspNetCore.OpenApi(+ Scalar UI) into the NativeAotMovieRating sample and updates docs/tests.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Light.PortableResults.AspNetCore.Mvc.Tests/UnitTests/ProducesPortableAttributesTests.cs | Adds unit tests for new MVC OpenAPI response attributes. |
| tests/Light.PortableResults.AspNetCore.Mvc.Tests/IntegrationTests/RegularMvcController.cs | Updates integration controller to new success helper + standard ASP.NET Core success docs. |
| tests/Light.PortableResults.AspNetCore.MinimalApis.Tests/PortableResultsEndpointExtensionsTests.cs | Updates Minimal API tests for new helper surface and failure helpers. |
| src/Light.PortableResults.AspNetCore.Shared/WrappedResponse.cs | Removes old success OpenAPI schema type. |
| src/Light.PortableResults.AspNetCore.Shared/PortableSuccessResponse.cs | Adds new success OpenAPI schema type. |
| src/Light.PortableResults.AspNetCore.Shared/PortableError.cs | Adds schema-only error item types for OpenAPI. |
| src/Light.PortableResults.AspNetCore.Shared/PortableValidationErrorDetail.cs | Adds schema-only ASP.NET Core-compatible validation error detail types. |
| src/Light.PortableResults.AspNetCore.Shared/PortableProblemDetails.cs | Adds schema-only problem-details types for non-validation failures. |
| src/Light.PortableResults.AspNetCore.Shared/PortableRichValidationProblemDetails.cs | Adds schema-only rich validation problem-details types. |
| src/Light.PortableResults.AspNetCore.Shared/PortableAspNetCoreValidationProblemDetails.cs | Adds schema-only ASP.NET Core-compatible validation problem-details types. |
| src/Light.PortableResults.AspNetCore.Mvc/ProducesPortableSuccessResponseAttribute.cs | Adds renamed MVC success response attribute. |
| src/Light.PortableResults.AspNetCore.Mvc/ProducesPortableProblemAttribute.cs | Adds MVC attributes for problem-details failures. |
| src/Light.PortableResults.AspNetCore.Mvc/ProducesPortableRichValidationProblemAttribute.cs | Adds MVC attributes for rich validation failures. |
| src/Light.PortableResults.AspNetCore.Mvc/ProducesPortableAspNetCoreValidationProblemAttribute.cs | Adds MVC attributes for ASP.NET Core-compatible validation failures. |
| src/Light.PortableResults.AspNetCore.Mvc/ProducesPortableResultAttribute.cs | Removes old MVC success attributes (incl. single-generic). |
| src/Light.PortableResults.AspNetCore.MinimalApis/PortableResultsEndpointExtensions.cs | Renames success helper + adds failure/validation OpenAPI helpers. |
| src/Light.PortableResults.AspNetCore.MinimalApis/packages.lock.json | Updates dependency version range for Light.PortableResults. |
| samples/NativeAotMovieRating/Program.cs | Adds OpenAPI generation + Scalar UI endpoints to sample app. |
| samples/NativeAotMovieRating/NativeAotMovieRating.csproj | Adds OpenAPI/Scalar package references. |
| samples/NativeAotMovieRating/packages.lock.json | Locks new OpenAPI/Scalar dependencies and updates project dependency ranges. |
| samples/NativeAotMovieRating/requests.http | Adds sample requests for OpenAPI JSON + Scalar UI. |
| samples/NativeAotMovieRating/JsonSerialization/MovieRatingJsonContext.cs | Registers OpenAPI-related schema types/primitive types for source-gen JSON in AOT. |
| samples/NativeAotMovieRating/GetMovies/GetMoviesEndpoint.cs | Adds OpenAPI metadata (name/tags/summary/produces) for endpoint. |
| samples/NativeAotMovieRating/AddMovieRating/AddMovieRatingEndpoint.cs | Adds OpenAPI metadata (name/tags/summary/produces) for endpoint. |
| ai-plans/0040-openapi-support.md | Adds the tracked implementation plan for issue #40. |
| README.md | Documents the renamed success surface and new OpenAPI helpers. |
| Light.PortableResults.slnx | Includes the new AI plan document in the solution. |
| Directory.Packages.props | Adds central package versions for Microsoft.AspNetCore.OpenApi and Scalar.AspNetCore. |
| /// <summary> | ||
| /// Gets or sets the error category. | ||
| /// </summary> | ||
| public ErrorCategory Category { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the optional metadata associated with the error. | ||
| /// </summary> | ||
| public object? Metadata { get; init; } |
There was a problem hiding this comment.
PortableError.Category is non-nullable, but the runtime JSON writer omits the category property when the category is Unclassified. Marking this as required will make the generated OpenAPI schema stricter than the actual response shape; consider making Category nullable/optional (and documenting that missing means Unclassified).
| /// <summary> | ||
| /// Gets or sets the top-level problem metadata. | ||
| /// </summary> | ||
| public TProblemMetadata Metadata { get; init; } = default!; |
There was a problem hiding this comment.
Problem details metadata is optional at runtime (it is only written when result metadata exists), but this schema type makes Metadata required via non-nullable TProblemMetadata. Consider making it nullable/optional so OpenAPI matches the actual serialized response.
| public TProblemMetadata Metadata { get; init; } = default!; | |
| public TProblemMetadata? Metadata { get; init; } = default; |
| /// <summary> | ||
| /// Gets or sets the top-level problem metadata. | ||
| /// </summary> | ||
| public TProblemMetadata Metadata { get; init; } = default!; |
There was a problem hiding this comment.
Rich validation problem details metadata is optional at runtime (only written when present), but this schema type makes Metadata required via non-nullable TProblemMetadata. Consider making it nullable/optional so OpenAPI matches the actual serialized response.
| public TProblemMetadata Metadata { get; init; } = default!; | |
| public TProblemMetadata? Metadata { get; init; } = default; |
| /// Gets or sets the metadata associated with the error detail. | ||
| /// </summary> | ||
| public TMetadata Metadata { get; init; } = default!; |
There was a problem hiding this comment.
In ASP.NET Core compatible validation responses, errorDetails[].metadata is optional (entries may exist due to code/category without metadata). In the typed schema variant, Metadata is non-nullable/required, which can misdocument responses. Consider making Metadata nullable/optional (TMetadata?).
| /// Gets or sets the metadata associated with the error detail. | |
| /// </summary> | |
| public TMetadata Metadata { get; init; } = default!; | |
| /// Gets or sets the optional metadata associated with the error detail. | |
| /// </summary> | |
| public TMetadata? Metadata { get; init; } |
| [ProducesPortableRichValidationProblem] | ||
| [ProducesPortableProblem(statusCode: StatusCodes.Status404NotFound)] | ||
| [ProducesPortableProblem] | ||
| public async Task<LightActionResult<MovieRating>> AddMovieRating(AddMovieRatingDto dto) |
There was a problem hiding this comment.
The MVC example references AddMovieRatingDto, but there is no such type in the sample code (the sample uses MovieRatingDto). This makes the documentation example inconsistent/non-compilable; update the parameter type to the correct DTO used by the library/sample.
| public async Task<LightActionResult<MovieRating>> AddMovieRating(AddMovieRatingDto dto) | |
| public async Task<LightActionResult<MovieRating>> AddMovieRating(MovieRatingDto dto) |
| /// Gets or sets the metadata associated with the error. | ||
| /// </summary> | ||
| public TMetadata Metadata { get; init; } = default!; |
There was a problem hiding this comment.
In the runtime rich error JSON shape, the metadata property is omitted when no metadata exists. In the typed schema variant (PortableError<TMetadata>), Metadata is currently non-nullable/required, which can make OpenAPI docs inaccurate for errors that have no metadata. Consider making Metadata nullable (e.g., TMetadata?) so the schema matches the payload.
| /// Gets or sets the metadata associated with the error. | |
| /// </summary> | |
| public TMetadata Metadata { get; init; } = default!; | |
| /// Gets or sets the optional metadata associated with the error. | |
| /// </summary> | |
| public TMetadata? Metadata { get; init; } |
| /// <summary> | ||
| /// Gets or sets the top-level problem metadata. | ||
| /// </summary> | ||
| public TProblemMetadata Metadata { get; init; } = default!; |
There was a problem hiding this comment.
Top-level problem metadata is optional at runtime (only written when present), but this schema type makes Metadata required via non-nullable TProblemMetadata. Consider making it nullable/optional so OpenAPI matches the actual serialized response.
| public TProblemMetadata Metadata { get; init; } = default!; | |
| public TProblemMetadata? Metadata { get; init; } |
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
…eRating project Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
…kages.lock.json Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
…former Signed-off-by: Kenny Pflug <[email protected]>
…move internal IPortableSuccessResponseOpenApiAttribute Signed-off-by: Kenny Pflug <[email protected]>
…ormer Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
…ment equality in PortableErrorMetadataContract class hierarchy Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
…shCode Signed-off-by: Kenny Pflug <[email protected]>
…tionary Signed-off-by: Kenny Pflug <[email protected]>
…tion delegate in AddPortableResultsOpenApi Signed-off-by: Kenny Pflug <[email protected]>
…a factories Replace the nine generic CLR-surrogate record types (InRangeMetadata<T>, EqualToMetadata<T>, etc.) and their JsonSchemaExporter-based code path with programmatic OpenApiSchema construction, eliminating the NotSupportedException that occurred under NativeAOT. Key changes: - Add PortableOpenApiSchemaTypeMapper mapping CLR primitives → OpenApiSchema - Replace InlineErrorMetadataTypes (Type[]) with InlineErrorMetadataContracts (PortableErrorMetadataContract[]) on the attribute base class - Replace AppendTypes with AppendContracts in builder utilities - Add WithErrorMetadata(code, Func<OpenApiSpecVersion,OpenApiSchema>, [CallerArgumentExpression] diagnosticName) overload to both builder classes - Fix PortableErrorMetadataSchemaContract equality/hash to use DiagnosticName (ordinal) instead of lambda reference equality - Rewrite all 18 BuiltInValidationErrorBuilderExtensions helpers to use schema factories via PortableOpenApiSchemaTypeMapper - Delete BuiltInValidationErrorMetadata.cs (nine dead generic record types) - Update transformer inline loop and ValidateInlineMetadataArrays accordingly - Update all affected tests; delete BuiltInValidationErrorMetadataTests.cs; add idempotency test for repeated typed-helper registration - Add NativeAOT OpenAPI fix plan document Co-authored-by: Copilot <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
…tract, remove corresponding CallerArgumentExpressionAttribute Signed-off-by: Kenny Pflug <[email protected]>
…ce checks Signed-off-by: Kenny Pflug <[email protected]>
…ample Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
…ot be found Signed-off-by: Kenny Pflug <[email protected]>
Signed-off-by: Kenny Pflug <[email protected]>
Minimum allowed line rate is |
Closes #40
Update on 2026-04-30
The original plan treated OpenAPI support as a thin, schema-only CLR layer on top of the existing ASP.NET Core integrations. The final implementation went in a different direction: OpenAPI became its own opt-in package, schema generation moved from surrogate CLR types to a document transformer plus a library-authored schema catalog, validation error contracts became code-driven and registry-based, and later follow-up plans tightened the design for package boundaries, NativeAOT, coverage, and downstream OpenAPI tooling behavior.
The most important architectural change is that OpenAPI is no longer modeled primarily through public CLR response types such as
PortableProblemDetails<TErrorMetadata, TProblemMetadata>orPortableSuccessResponse<TValue, TMetadata>. Instead, the library now owns the OpenAPI document directly and synthesizes response schemas from endpoint metadata.Major Deviations From The Original Plan
1. OpenAPI moved out of the runtime ASP.NET Core packages into dedicated opt-in packages
Original plan:
Light.PortableResults.AspNetCore.Sharedwould contain the schema-only OpenAPI CLR types,Light.PortableResults.AspNetCore.MinimalApiswould expose theRouteHandlerBuilderhelpers, andLight.PortableResults.AspNetCore.Mvcwould expose the response metadata attributes. No separate OpenAPI package was introduced.Implemented direction:
OpenAPI support was moved into a new dedicated package,
Light.PortableResults.AspNetCore.OpenApi, with its own service-registration entry point,AddPortableResultsOpenApi(). The runtime packagesLight.PortableResults.AspNetCore.MinimalApisandLight.PortableResults.AspNetCore.Mvcno longer expose the OpenAPI helper or attribute surface at all. A second bridge package,Light.PortableResults.Validation.OpenApi, was later added for validation-specific built-in error contracts. The redesign also explicitly targetsMicrosoft.AspNetCore.OpenApi; Swashbuckle / NSwag-specific integration is not part of the public surface.Impact:
This is a major packaging and layering deviation. The final design keeps the runtime packages free of the
Microsoft.AspNetCore.OpenApidependency and makes OpenAPI support an explicit opt-in concern instead of part of the core ASP.NET Core integration surface.2. The schema-only CLR surrogate model was abandoned entirely
Original plan:
The public OpenAPI model was centered around schema-only CLR types such as:
PortableSuccessResponse<TValue, TMetadata>PortableErrorandPortableError<TMetadata>PortableValidationErrorDetailandPortableValidationErrorDetail<TMetadata>PortableProblemDetails<TErrorMetadata, TProblemMetadata>PortableRichValidationProblemDetails<TErrorMetadata, TProblemMetadata>PortableAspNetCoreValidationProblemDetails<TErrorDetailMetadata, TProblemMetadata>OpenAPI generators were expected to infer schemas from those CLR types.
Implemented direction:
That entire surrogate model was removed. The library now authors canonical OpenAPI schemas directly through
PortableResultsOpenApiSchemasand usesPortableResultsOpenApiDocumentTransformerto install canonical components and synthesize operation-specific derived schemas.Impact:
This is the core architectural pivot. It avoids generic CLR type names leaking into OpenAPI component ids, removes the need for alias hierarchies and naming workarounds, and stops promising metadata CLR shapes that the runtime HTTP writers do not actually enforce.
3. The success-response design changed from a metadata-generic CLR wrapper to a mode-aware single-generic helper
Original plan:
The success-side OpenAPI helper existed only for the wrapped
{ value, metadata }body shape and always required an explicit metadata type throughPortableSuccessResponse<TValue, TMetadata>,ProducesPortableSuccessResponse<TValue, TMetadata>, andProducesPortableSuccessResponseAttribute<TValue, TMetadata>. PlainTValuesuccess responses were supposed to use standard ASP.NET Core OpenAPI APIs.Implemented direction:
The final design collapsed the public success helper to
ProducesPortableSuccessResponse<TValue>andProducesPortableSuccessResponseAttribute<TValue>. The generated success schema is now selected from the effectiveMetadataSerializationMode: underErrorsOnlyit documents the bareTValueresponse shape, and underAlwaysit synthesizes a wrapped{ value, metadata }envelope. Top-level metadata can still be narrowed explicitly, but it is no longer a public generic parameter on the helper surface.Impact:
This is both an API-shape deviation and a behavioral one. The success-side OpenAPI surface is now mode-aware and can follow the application default from
PortableResultsHttpWriteOptions, which is more dynamic than the strictly static, metadata-generic model described in0040-0. The transient rename fromWrappedResponse<TValue, TMetadata>toPortableSuccessResponse<TValue, TMetadata>became a short-lived intermediate state rather than the final contract.4. Separate validation helper families were collapsed into one validation helper with format selection
Original plan:
Minimal APIs and MVC would expose separate helper/attribute families for:
The split was intentional so callers had to choose the exact validation schema shape explicitly.
Implemented direction:
The final public surface exposes only:
ProducesPortableProblemProducesPortableValidationProblemProducesPortableProblemAttributeProducesPortableValidationProblemAttributeThe effective validation schema is resolved from
PortableResultsHttpWriteOptions.ValidationProblemSerializationFormator a per-endpoint/per-attribute override. The MVC attributes are no longerProducesResponseTypeAttribute<TSchema>wrappers; they are custom endpoint metadata attributes consumed directly by the OpenAPI document transformer.Impact:
This is a real API simplification relative to the original plan. Instead of encoding the validation format in the helper name, the final design keeps one validation helper and lets the transformer choose the canonical validation schema based on the effective format.
5. Metadata typing moved from public generic parameters to explicit schema narrowing and a contract registry
Original plan:
Metadata typing was expressed directly in public generic parameters such as
TErrorMetadata,TErrorDetailMetadata, andTProblemMetadata. The documented contract for metadata was therefore tied to CLR generic arguments on the public API.Implemented direction:
The final design treats metadata slots as open objects by default and narrows them explicitly only when the caller opts in. Top-level metadata narrowing is attached through endpoint metadata. Per-error-code metadata narrowing is driven through
ConfigureErrorMetadataContracts(...),PortableErrorMetadataContractsBuilder,IPortableErrorMetadataContractRegistry, and inlineWithErrorMetadata(...)overrides.This later expanded again in
0040-2, where contracts were widened from "CLR type only" to a closed discriminated union:Impact:
This is a substantial conceptual deviation. The OpenAPI layer no longer assumes that one public CLR generic argument can faithfully describe the runtime metadata shape. Instead, metadata documentation is selective, per-endpoint, and often per-error-code.
6. Error-code-specific contracts became a first-class part of the OpenAPI model
Original plan:
The plan documented only coarse response envelopes. It did not define a registry for specific error codes, code-discriminated unions, or endpoint-level narrowing of
errors[*].metadataanderrorDetails[*].metadata.Implemented direction:
The final design introduced error-code-aware OpenAPI generation. Endpoints can declare documented codes through
WithErrorCodes(...), register global metadata contracts in DI, and add inline per-endpoint metadata contracts for specific codes. The transformer emits per-code schema variants, discriminator mappings, and narrowed response envelopes.0040-2then went further by addingValidationErrorCodes,BuiltInValidationErrorContracts, andRegisterBuiltInValidationErrors()so the built-in validation taxonomy is available as a reusable OpenAPI contract catalog instead of requiring every consumer to redeclare it.Impact:
This is not just a deviation but a major expansion beyond the original plan. The final OpenAPI surface documents individual error-code contracts rather than only top-level problem-envelope shapes.
7. Validation-specific OpenAPI support became a bridge package with built-in catalogs and typed helpers
Original plan:
Validation support was limited to documenting one of two validation problem envelope shapes through the shared schema-only CLR types.
Implemented direction:
0040-2introducedLight.PortableResults.Validation.OpenApias a dedicated bridge package. That package owns:RegisterBuiltInValidationErrors()ValidationErrorCodesWithInRangeError<T>(),WithGreaterThanError<T>(), and related helpers for site-specific narrowing of polymorphic built-in codesThe plan also renamed several validation error codes for clarity:
LengthInbecameLengthInRange,MatchesbecamePattern,IsInBetweenbecameInRange, andNotInBetweenbecameNotInRange.Impact:
This is a broader validation/OpenAPI integration model than
0040-0described. OpenAPI documentation for validation is now organized around a shared framework-owned code taxonomy plus optional endpoint-level narrowing.8. NativeAOT forced another design pivot away from CLR surrogates used by typed validation helpers
Original plan:
The original plan did not center NativeAOT as a design constraint for OpenAPI schema generation.
Implemented direction:
0040-4found that the typed validation helper path still relied on CLR record surrogates flowing through the ASP.NET Core schema generator, which breaks in NativeAOT unless every generated type is in the application'sJsonSerializerContext. The fix was to delete those helper-only CLR record surrogates and switch the typed validation helpers to schema-factory contracts instead. The publicPortableOpenApiSchemaTypeMapperwas added to map CLR primitive-like types toOpenApiSchema, and inline endpoint metadata now storesPortableErrorMetadataContractvalues rather than justTypevalues.Impact:
This is another strong deviation from the CLR-surrogate mindset of
0040-0. Even where the redesign had briefly kept CLR types for endpoint-scoped narrowing, the final direction removed them in favor of schema factories so the OpenAPI stack remains NativeAOT-compatible.9. The final error-union model became exhaustive-by-default and the derived envelopes were flattened
Original plan:
The original plan did not describe per-error-code discriminated unions at all. It also assumed inheritance/composition through CLR schema types rather than transformer-authored flattened schemas.
Implemented direction:
0040-5tightened the document model again:AllowUnknownErrorCodes()is the explicit opt-out for non-exhaustive endpointsoneOfwithout a fallback branch in exhaustive modeallOfcomposition against the canonical envelopePortableResultsOpenApiSchemasbecame the single source of truth for both canonical and derived envelopesImpact:
The final document shape is considerably more precise than
0040-0envisioned and is tuned for downstream tooling behavior in Swagger UI, Scalar, Kiota, NSwag, and openapi-generator. This is another area where the implementation went materially beyond the original plan rather than simply implementing it differently.10. The testing strategy changed from package-local helper tests to package-scoped, document-generation-heavy coverage
Original plan:
The plan expected tests for the Minimal API helpers, MVC attributes, and the renamed success helpers inside the existing ASP.NET Core test projects, with a new MVC test class for attribute metadata.
Implemented direction:
The final design introduced dedicated package-oriented test coverage:
Light.PortableResults.AspNetCore.OpenApi.TestsLight.PortableResults.Validation.OpenApi.Tests0040-3explicitly reorganized the tests around those package boundaries, preferred sociable in-memory OpenAPI document-generation tests over isolated surface checks, and tracked coverage withcoverage.runsettingsso generated files do not distort the numbers.Impact:
This is a practical deviation in delivery strategy. The tests now validate the transformer-driven package design end to end instead of primarily asserting helper registration behavior in the original runtime packages.
Original Intent That Survived
Not everything changed. Two important parts of
0040-0still describe the final design accurately:LightResult,LightResult<T>,LightActionResult,LightActionResult<T>, or the JSON writers inLight.PortableResults.Net Result
The original plan was a CLR-type-centric OpenAPI layer embedded into the ASP.NET Core runtime packages. The implemented direction is a package-separated, transformer-driven, error-code-aware OpenAPI system with validation-specific bridge packages, NativeAOT-safe schema factories, and a more precise final schema model for downstream tooling.
In short:
0040-0proposed "document PortableResults by exposing schema-only CLR response types." The final implementation became "generate an OpenAPI document directly from explicit endpoint metadata and library-owned schema building blocks."