Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add built-in support for generating OpenAPI document from APIs #54598

Open
8 of 21 tasks
captainsafia opened this issue Mar 18, 2024 · 63 comments
Open
8 of 21 tasks

Add built-in support for generating OpenAPI document from APIs #54598

captainsafia opened this issue Mar 18, 2024 · 63 comments
Assignees
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates Epic Groups multiple user stories. Can be grouped under a theme. feature-openapi
Milestone

Comments

@captainsafia
Copy link
Member

captainsafia commented Mar 18, 2024

This issue outlines background content, proposed design, and proposed API to add support for built-in OpenAPI document generation to minimal APIs and MVC in the Microsoft.AspNetCore.OpenApi package. It also captures some open questions and future plans for work in this space. Some of the implementation details are subject to change as work evolves.

Background

The OpenAPI specification is a standard for describing HTTP APIs. The standard allows developers to define the shape of APIs that can be plugged into client generators, server generators, testing tools, documentation and more. Despite the universality and ubiquity of this standard, ASP.NET Core does not provide support for OpenAPI by default within the framework.

ASP.NET Core does not provide first-class, built-in support for OpenAPI. Instead, ASP.NET Core has shipped with support for ApiExplorer (not to be confused with Visual Studio's API Explorer) for quite some time. The ApiExplorer is a helpful abstraction that provides metadata about the routes that are registered in an application. This metadata is accessible via the DI container and is used by tools in the ecosystem like Asp.Api.Versioning, NSwag, and Swashbuckle to introspect and query the metadata aggregated by ApiExplorer.

In .NET 6, minimal APIs was introduced and support for minimal APIs was added to ApiExplorer via the EndpointMetadataApiDescriptionProvider which allowed querying the ApiExplorer metadata to introspect registered minimal endpoints in an application.

In .NET 7, the Microsoft.AspNetCore.OpenApi package was introduced (note: this package ships via NuGet and is not part of the shared framework). It exposed the WithOpenApi extension method for modifying the OpenApiOperation associated with a single endpoint in minimal APIs. The package takes a dependency on the Microsoft.OpenApi package which provides an object model and deserializers/serializers for interacting with various versions of the OpenAPI specification.

The evolution of our OpenAPI "support" has resulted in a large quantity of bugs and feature gaps (ref). To resolve this and provide a more seamless experience for users, we're incorporating OpenAPI document generation as a first-class feature in ASP.NET Core.

Future Implementation

Implementation Overview

The flow diagram outlines the proposed implementation. New components are in a bordered box. The OpenApiComponentService is responsible for managing state that will be serialized to the top-level components field in the OpenAPI document. At the moment, it is largely responsible for generating and managing JSON schemas associated with application types. The OpenApiDocumentService exposes a GetOpenApiDocument method for resolving the OpenApiDocument associated with an application. These new components build on top of the metadata that is produced by the ApiExplorer, allowing us to take advantage of a time-tested and well-established component.

flowchart LR
	mvc[Controller-based API]
	minapi[Minimal API]
	defdescprov[DefaultApiDescriptionProvider]
	endpdescprov[EndpointMetadataDescriptionProvider]
	desccoll[IApiDescriptionCollection]
	compservice[[OpenApiComponentService]]
	docsvc[[OpenApiDocumentService]]
	idocprovider[[IDocumentProvider]]
	meas[Microsoft.Extensions.ApiDescription.Server]
	mvc --> defdescprov 
	mvc --> endpdescprov
	minapi --> endpdescprov
	defdescprov --> desccoll
	endpdescprov --> desccoll
	desccoll --> docsvc
	idocprovider --> docsvc
	meas --> idocprovider
	compservice --> docsvc

Document Generation

The OpenAPI document contains a well-defined set of fields to be populated with established meanings. To make it easier to understand why documents are generated the way they are, we intend to document and implement the following semantics are used when generating the OpenAPI document.

  • info.name: Derived from the entry-point assembly name.
  • info.version: Defaults to 1.0.
    • Future: integrate with versioning information in Asp.Versioning.
  • servers: Derived from the host info registered in the IServer implementation.
  • paths: Aggregated from ApiDescriptions defined in the ApiExplorer
    • paths.operations.parameters: captures all routes understood by the model binding systems of MVC and minimal except inert parameters like those coming from the DI container
      • Information in route constraints and validation attributes will be added onto the schemas associated with each parameter type
  • components: Aggregates JSON schemas from the OpenApiComponentService (see below)
  • security: No autogeneration by default.
  • tags: Aggregates all tags discovered during the construction of the document.

JSON Schema Generation

OpenAPI definitions rely on a derivative implementation of JSON Schema to describe the shapes of types that are used in request parameters and responses. .NET does not contain a built-in solution for generating or validating JSON schemas from .NET types (although this work is outlined here). To fill this gap, this implementation will ship with an OpenApiComponentService that uses System.Text.Json's type resolver APIs to generate the underlying OpenAPI schemas. This gives us the opportunity to address some of the gaps that exist with how certain types are currently implemented as seen in the following issues:

  • Correct handling duplicate FromForm parameters in a given API (ref)
  • Correct handling nullability annotations for response types (ref)
  • Correctly handling IFormFile and IFormFileCollection inputs
  • Correctly handling polymorphic serialization metadata from STJ in the generated schema using oneOf and type discriminaotrs
  • Correctly handling inheritance hierarchies in types using allOf
  • Applying all supported route constraints to generated schemas
  • Applying all supported ComponentModel annotations to generated schemas

Note: The version of OpenAPI.NET that we intend to target uses the JsonSchema.NET to handle JSON schema representation in the OpenAPI document.

Question: As part of this work, we'll build a test bed to validate schema generation behavior. Please share examples of APIs you'd like to make sure appropriate schemas are generated for so they can be included in the test set.

Generating Operation IDs

The OpenAPI specification consists of operations that uniquely identify an endpoint by it's operation type (HTTP method) and path. Each of these operations can optionally include an operation ID, a unique string that identifies the operation. Operation IDs play an important role in OpenAPI integrations with client generators, OpenAI plugins, and more. Users can define operation IDs for each endpoint in their application themselves, but ideally we should be able to generate high-quality operation IDs by default to make the process more seamless for the user. Operation IDs should be deterministic so it's not sufficient to generate an arbitrary GUID for each operation in an application. The proposed semantics for generated operation IDs are as follows:

  • If a name is provided on an action via the route method, use that name.
  • If a name is provided on an action or endpoint using the EndpointName metadata, use that name.
  • If neither is available, attempt to create an operation ID using information available in the route template and endpoint metadata in sequential order.
    • The HTTP method associated with the operation stringified (GET, POST, etc.)
    • If route segments exist on the application, the route segment values concatenated by _.
    • If the route segments contain parameters, use the parameter name sans any constraints.
  • If duplicate operation IDs are generated with these semantics, disambiguate them with a monotonically increasing integer (Get_1, Get_2, etc.)

Swagger UI (Or Lack Thereof)

The web templates currently expose two endpoints by default in relation to OpenAPI: one that serves the OpenAPI document as a JSON file and another that serves an Swagger UI against the backing JSON file. At the moment, we don't intend to ship with Swagger UI as a supported UI in ASP.NET Core. Although it provides the benefit of an accessible UI for ad-hoc testing, it introduces engineering overhead around shipping (need to bundle web assets), has some security implications (it's easy to accidently leak client secrets for certain authentication configurations), and introduces maintenance overhead (need to make sure that we upgrade swagger-ui as needed).

Since swagger-ui is independent of the OpenAPI document, users can independently incorporate into their applications if needed via third-party packages or their own code (swagger-ui just needs a pointer to the served OpenAPI JSON file). Users can also take advantage of other ad-hoc testing tools that plug in to OpenAPI, like ThunderClient.

Customizing document generation

The automatic document generation will make use of metadata exposed via ApiExplorer to generate the OpenAPI document. There are currently several avenues that exist in the framework for influencing the generation of this document:

  • Accepts and Produces metadata allow limited support for customizing the content-types and object types associated with an endpoints parameters and responses
  • EndpointName, EndpointTags, EndpointSummary, and EndpointDescription metadata and their associated attributes/extension methods allow customization of the tags, summary, and description fields associated with a request
  • WithOpenApi extension method supports overriding the OpenApiOperation associated with an endpoint in its entirety

The customization story is disparate at the moment, and it's largely a result of the way the system evolved. As we move to support generating entire documents, there are certain aspects we don't provide APIs for customizing, like the version number specified in the info of the OpenAPI document or the supported security schemes. This effort provides a nice avenue for unifying the various strategies that have proliferated in the codebase for customizing these aspects.

The current customization options that we provide are largely endpoint-focused, mostly because we've never had the need to manage document-level settings like properties in the info property of the OpenAPI document.

XML Documentation Support

One of the most upvoted issues with regards to OpenAPI/ApiExplorer in our repo is around supporting XML code comments (ref). Being able to generate the OpenAPI document as standard functionality pushes us towards support for this feature. Work here will requiring reading the generated XML documentation at runtime, mapping members to the appropriate operations, and merging information from the XML comment into the target operation.

Ecosystem Integration

OpenAPI plays an important role into several existing technologies in the space. At the center of this effort is the goal to produce a high-quality OpenAPI document that provides strong integrations with existing tools in the ecosystem including:

  • Kiota client generation
  • NSwag generators
  • Asp.Api.Versioning
  • OpenAI plugins
  • Swagger UI/Redoc and other ad-hoc testing tools

Question: Are there other components we should validate integration with?

Build Time OpenAPI Document Generation

As far as build time generation goes, we'll plug-in to the existing integration provided by the dotnet-getdocument command line tool. dotnet-getdocument enforces a loose, string-based contract that requires an implementation of Microsoft.Extensions.ApiDescription.IDocumentProvider to be registered in the DI container. This means that users will be able to generate OpenAPI files at build-time using the same strategy they currently do by enabling the following MSBuild flags.

<OpenApiGenerateDocumentsOnBuild>true</OpenApiGenerateDocumentsOnBuild>
<OpenApiDocumentsDirectory>.\</OpenApiDocumentsDirectory>

Behind the scenes, this functionality works by booting up the entry point assembly behind the scenes with an inert IServer implementation, querying the DI container on the entry point's host for the IDocumentProvider interface, then using reflection to invoke the appropriate methods on that interface.

Note: although I've explored strategies for generating OpenAPI documents at build-time using static analysis, this is out-of-scope for the time being.

Runtime OpenAPI Document Generation

Templates will be updated to include the following code in Program.cs when OpenAPI is enabled. The AddOpenApi method registers the appriopriate OpenAPI-related services and MapOpenApi exposes an endpoint that serves the serialized JSON document.

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/", () => "Hello world!");

app.Run();

Underlying API

See #54600 for full API review.

// Assembly: Microsoft.AspNetCore.OpenApi
namespace Microsoft.AspNetCore.Builder;

public static class IEndpointRouteBuilderExtensions
{
  public static IEndpointRouteBuilder MapOpenApi(this IEndpointRouteBuilder builder);
}
// Assembly: Microsoft.AspNetCore.OpenApi
namespace Microsoft.Extensions.DependencyInjection;

public static class IServiceCollectionExtensions
{
  public static IServiceCollection AddOpenApi(this IServiceCollection serviceCollection);
  public static IServiceCollection AddOpenApi(this IServiceCollection serviceCollection, Action<OpenApiOptions> configureOptions);
}
// Assembly: Microsoft.AspNetCore.OpenApi
namespace Microsoft.AspNetCore.OpenApi;

public class OpenApiOptions
{
  public string JsonFilePath { get; set; }
  public OpenApiSpecVersion OpenApiVersion { get; set; }
}

Tasks

P0: Basic Functionality (preview4)

  • Support generating OpenAPI document using ApiExplorer metadata
  • Support generating JSON schemas using System.Text.Json schema generation support and Microsoft.OpenAPI
  • Support serving JSON OpenAPI document from server
  • Support generating OpenAPI document at build with Microsoft.Extensions.ApiDescription.Server infrastructure
  • Support customization of OpenAPI document (document and operations)

P0: Basic Functionality Follow-ups (preview5)

  • Replat WithOpenApi on top of operation transformers
  • Add support for schema filters
  • Update web API templates to use new APIs
  • Update gRPC JSON transcoding implementation
  • Add support for reference IDs and schema references
  • Verify native AoT comapt for minimal APIs scenarios

P1: Ecosystem Integration and Enhancements

  • Support incorporating API versions from Asp.Versioning
  • Validate generation experience with Kiota, NSwag
  • Support integrating XML comments into generated code
@captainsafia captainsafia added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates Epic Groups multiple user stories. Can be grouped under a theme. feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc labels Mar 18, 2024
@captainsafia captainsafia added this to the 9.0.0 milestone Mar 18, 2024
@cphorton
Copy link

Apologies if this is the wrong place to ask these questions, but will Yaml file generation be supported out of the box?

It would also be great to have the ability to perform a schema validation if you are doing schema first I.e. load a schema from a file and validate that the generated OpenAPI spec matches the loaded schema.

@captainsafia
Copy link
Member Author

Apologies if this is the wrong place to ask these questions, but will Yaml file generation be supported out of the box?

I haven't thought about YAML generation as something we'd ship as part of preview4. However, it should be trivial to support and something that we've discussed in the API review of the first batch of APIs (ref).

If folks will get a lot of value from supporting YAML generation by default, I'd be happy to add support for it.

@cphorton
Copy link

If folks will get a lot of value from supporting YAML generation by default, I'd be happy to add support for it.

Thanks for your swift response.

I understand that the objective here is not necessarily a like for like replacement for Swashbuckle, but Yaml was one of the formats that was supported by that library, you just needed to request the swagger.json spec from the endpoint with a .yaml extension

@captainsafia
Copy link
Member Author

I understand that the objective here is not necessarily a like for like replacement for Swashbuckle, but Yaml was one of the formats that was supported by that library, you just needed to request the swagger.json spec from the endpoint with a .yaml extension

Yep, the serialization functionality in both places is actually supported by the underlying Microsoft.OpenAPI package so support for YAML is largely a manner of wiring it up and calling the right APIs. That being said, I want to be careful about introducing too many configurability toggles early on hence the ask for user feedback here. Let's use the thumbs up reaction on this comment to signal that having YAML serialization before GA would be valuable.

@captainsafia captainsafia self-assigned this Mar 18, 2024
@RicoSuter
Copy link

RicoSuter commented Mar 18, 2024

.NET does not contain a built-in solution for generating or validating JSON schemas from .NET types (although this work is outlined here)

The link in “here” seems to be wrong, can you provide the right one?

Note: The version of OpenAPI.NET that we intend to target uses the JsonSchema.NET to handle JSON schema representation in the OpenAPI document.

Can you explain this? What is OpenAPI.NET? I thought you will use your Microsoft.OpenApi package?

Will you use/reference JsonSchema.NET from the asp package to generate schemas? Why not eg NJsonSchema (disclaimer: I’m the author)?

Best would be to split up Microsoft.OpenApi into Microsoft.OpenApi and System.Text.Json.Schema and then let other libs extend generators with filters/processors. For example OSS could provide an extension for xml docs (I did that already for NSwag/NJsonSchema with Namotion.Reflection)

@captainsafia
Copy link
Member Author

The link in “here” seems to be wrong, can you provide the right one?

Ooops. That should've been a link to dotnet/runtime#29887. 😅

Can you explain this? What is OpenAPI.NET? I thought you will use your Microsoft.OpenApi package?

Yes, I use the terms OpenAPI.NET and Microsoft.OpenApi interchangeably. They refer to the same package though.

Will you use/reference JsonSchema.NET from the asp package to generate schemas? Why not eg NJsonSchema (disclaimer: I’m the author)?

Yes, we're planning on taking a dependency on the v2 version of the Microsoft.OpenApi package for GA which takes a dependency on JsonSchema.NET's APIs for representing the schemas so the two fit together well.

Best would be to split up MS.OpenApi into OAI and System.TextJson.Schema and then let other libs extend generators with filters/processors.

I'm understanding this question is related to the relationship between JSON Schema generation and the OpenAPI document.

Assuming I understood the question correctly, the challenge with this as I see it there's two aspects to the JSON schema component. You need the object model to represent the JSON schema within your OpenAPI document and then you need the reflection-based APIs to support generating JSON schemas from .NET types. At the moment, the two are deeply intertwined as there's no common JSON schema object model that the reflection-based APIs can write to.

@RicoSuter
Copy link

RicoSuter commented Mar 18, 2024

Yes, we're planning on taking a dependency on the v2 version of the Microsoft.OpenApi package for GA which takes a dependency on JsonSchema.NET's APIs for representing the schemas so the two fit together well.

Ok, I see the refs in Microsoft.OpenApi (you should probably not use another repo name than the package name :-)):

<ItemGroup>
  <PackageReference Include="JsonSchema.Net" Version="4.1.5" />
  <PackageReference Include="JsonSchema.Net.OpenApi" Version="1.1.0" />
</ItemGroup>

I think it's important that MS eventually also even owns the JSON Schema model and generate it as part of System.Text.Json, eventually even the JsonConverter should have a "ConvertSchema" method so the serializer metadata can fully and correctly describe its schemas... (/cc #29887)

Other tools should also be able to hook into the full schema generation process (schema, operation and document filters/processors).

@captainsafia
Copy link
Member Author

I think it's important that MS eventually also even owns the JSON Schema model and generate it as part of System.Text.Json, eventually even the JsonConverter should have a "ConvertSchema" method so the serializer metadata can fully and correctly describe its schemas...

Yep! Totally agree with this. I think JSON schema is foundational enough that coverage in STJ to resolve this would be much valued. But, for now, we have to work with the constraints of the ecosystem. 😅

Would definitely recommend chiming in to the issue in the dotnet/runtime repo with your thoughts around filters/processors since I haven't seen mention of that in that issue thread yet.

@swythan
Copy link

swythan commented Mar 18, 2024

Since swagger-ui is independent of the OpenAPI document, users can independently incorproate into their applications if needed via third-party packages or their own code (swagger-ui just needs a pointer to the served OpenAPI JSON file).

Will this be a tested and documented deliverable of the work you are doing here?

Exposing the Swagger UI on internal APIs is a deeply embedded workflow at my employer. It's often the lowest friction way to get information out of (or into) a system that doesn't really justify the expense of maintaining a dedicated UI.

@captainsafia
Copy link
Member Author

captainsafia commented Mar 18, 2024

@swythan Yes, Swagger UI operates fairly independently of the OpenAPI document generation pattern. Most of the magic is in bundling the static web assets needed by Swagger UI and giving it a pointer to the OpenAPI document.

Here's a quick sample doing this with a Minimal API to showcase what the packages are doing under the hood.

app.MapGet("/swagger", () => Results.Content("""
    <html>
    <head>
        <meta charset="UTF-8">
        <title>OpenAPI</title>
        <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
    </head>
    <body>
        <div id="swagger-ui"></div>

        <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
        <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>

        <script>
            window.onload = function() {
                const ui = SwaggerUIBundle({
                url: "/openapi.json", // or the URL where your OpenAPI document is located
                    dom_id: '#swagger-ui',
                    deepLinking: true,
                    presets: [
                        SwaggerUIBundle.presets.apis,
                        SwaggerUIStandalonePreset
                    ],
                    plugins: [
                        SwaggerUIBundle.plugins.DownloadUrl
                    ],
                    layout: "StandaloneLayout",
                })
                window.ui = ui
            }
        </script>
    </body>
    </html>
    """, "text/html"));

With regard to testing, a generated OpenAPI document that can plug into Postman/Redoc/Kioata/etc. should also be renderable in Swagger UI with no issues.

@desjoerd
Copy link

I think it's important that MS eventually also even owns the JSON Schema model and generate it as part of System.Text.Json, eventually even the JsonConverter should have a "ConvertSchema" method so the serializer metadata can fully and correctly describe its schemas...

Yep! Totally agree with this. I think JSON schema is foundational enough that coverage in STJ to resolve this would be much valued. But, for now, we have to work with the constraints of the ecosystem. 😅

Would definitely recommend chiming in to the issue in the dotnet/runtime repo with your thoughts around filters/processors since I haven't seen mention of that in that issue thread yet.

We use filters and customizations a lot on the openapi generation. Mainly around conventions like schema naming, but also to add the correct authentication schemes based on the authorization policies (including scopes). I am happy to share them, but will require some cleaning.

Another important thing is being able to generate multiple documents (also on build) based on custom logic, mainly around versioning or different types of apis (apis facing the front end vs backend, operations apis etc.)

@captainsafia
Copy link
Member Author

We use filters and customizations a lot on the openapi generation. Mainly around conventions like schema naming, but also to add the correct authentication schemes based on the authorization policies (including scopes). I am happy to share them, but will require some cleaning.

Yep! I see the use of filters for authentication schemes a lot. I actually did some experimentation in the space about two years ago and shared some of my notes on the experience in the comments of this issue. It's definitely not trivial to do and I'll have to rethink how we approach this if document generation is a built-in feature. I'm also a little bit cautious about being too automagical with anything authentication-related. 😅 But yes, asset built-in support for auto-generating auth schemes in the future, this is something we'll definitely need document-wide configurations for.

Another important thing is being able to generate multiple documents (also on build) based on custom logic, mainly around versioning or different types of apis (apis facing the front end vs backend, operations apis etc.)

I've been reasoning through support for multiple API versions using the Asp.Api.Versioning library. @desjoerd Are you using this in your codebase or a different tool? If you have a custom setup, I'd be curious to see how it works.

@desjoerd
Copy link

I do versioning the explict/KISS way, that is, checking the namespace of the endpoint whether a part contains a version. Split on '.' and looking for V{0}. And then duplicating (with the required modifications for a version bump) the supported Endpoints. During all past assignments which I've done it was rarely needed to do a v2, and when it was, it was of major changes. In almost all cases it was enough to just add fields for get, or add optional fields for post/put. Because changing the api (version bump) costs a lot, and that cost is mostly on the consumer side ^^.

In the current situation we have an sync api used by an app, and a management api to control assign tasks to users which will be synced. So a "public" api and an "internal" api. Splitting is done with endpoint tags.

The hardest part of splitting up the api with Swashbuckle (which we're currently using) are the schemas, and having one big schema repository which is shared between the generation of multiple documents.

@desjoerd
Copy link

I also want to add, custom json schema support is, I would say, a must. To support custom STJ converters, for example for GeoJSON.

@Lanayx
Copy link

Lanayx commented Mar 19, 2024

As an author of one of F# web frameworks around asp.net I'd like to ask for an extension method to explicitly specify inbound/return types of minimal API handler. And the reflection option to process those types, since source generators are unavailable in F#. This will be helpful for the frameworks where ReqeuestDelegate oveload is used rather than just Delegate.

@jimcward
Copy link

Just to add that we make fairly extensive usage of the currently available extension points to enhance/tweak our swagger output, things like:

  • IDocumentFilter
  • IOperationFilter
  • ISchemaFilter

It would leave us (and I am guessing others) in a difficult place if there weren't equivalent extensible points provided, thanks.

@captainsafia
Copy link
Member Author

Just to add that we make fairly extensive usage of the currently available extension points to enhance/tweak our swagger output, things like:

@jimcward I would be curious to learn about what kinds of modifications you're making with each filter type. Are the changes you are making tweaks to fix poorly generated schemas or modifications to enhance the generated documents (e.g. adding auth schemes).

@captainsafia
Copy link
Member Author

As an author of one of F# web frameworks around asp.net I'd like to ask for an extension method to explicitly specify inbound/return types of minimal API handler. And the reflection option to process those types, since source generators are unavailable in F#. This will be helpful for the frameworks where ReqeuestDelegate oveload is used rather than just Delegate.

@Lanayx I'm curious to learn more about this -- perhaps it's worthwhile to file another issue with a repro to discuss this. My understanding is that there should be metadata in the ApiExplorer types to help you with the above.

@jimcward
Copy link

Just to add that we make fairly extensive usage of the currently available extension points to enhance/tweak our swagger output, things like:

@jimcward I would be curious to learn about what kinds of modifications you're making with each filter type. Are the changes you are making tweaks to fix poorly generated schemas or modifications to enhance the generated documents (e.g. adding auth schemes).

Sure! So:

  • IDocumentFilter - so we use some IApplicationPartTypeProvider / ApplicationPart implementation to auto-generate some controllers for repetitive reference data via reflecting over DTOs - we've found that some of the output for auto generated controllers is less than ideal and so we can use an IDocumentFilter at the end of the process to tidy up certain aspects to make the output more sensible

  • IOperationFilter - similar to the above - when generating operations based on reflectively generated controllers the output is not ideal so we use an IOperationFilter to properly name parameters and operation summaries in a more readable format - this allows us to generate easier to understand documentation when reviewing available APIs via something like Swagger UI

  • ISchemaFilter - we use these for a few different reasons - one example is when we want to more clearly differentiate between the OpenAPI "date" and "date-time" schema types - by default DateTime will be output as "date-time" but we tag certain properties to highlight these expect only dates - we also use it to provide nicer output for schema elements that might have generic arguments as well as to apply "vendor extensions" where we may want endpoints to support things like power automate (for example, to apply dynamic values extensions: https://learn.microsoft.com/en-us/connectors/custom-connectors/openapi-extensions#use-dynamic-values )

Please let me know if you would like any more details on any of the above!

@captainsafia
Copy link
Member Author

@captainsafia does the team have any plans on how info attributes (particularly title and version) will be populated in the OpenAPI document? I was wondering about leveraging not only IHostEnvironment but also Microsoft.Extensions.AmbientMetadata.Application for fetching the information.

@julealgon Interesting idea! At the moment, the implementation doesn't take advantage of the AmbientMetadata properties. Document transformers provide a helpful escape hatch here. If there's customer feedback that relying on AmbientMetadata is the right approach here, I'm happy to bake it as part of the default experience.

I hope you will have a focus on accuracy. For me, Swashbuckle has always generated slightly incorrect OpenAPI documents by default, and NSwag much worse.

@jesperkristensen Thanks for the comment! This provided a helpful avenue for me to share some updates on how JSON schema is shaping in this front.

JSON schema support will land partially in System.Text.Json for .NET 9 (see dotnet/runtime#100159). The OpenAPI schemas generated in this implementation will build on top of the schemas generated by System.Text.Json. Hopefully, this will be a boon for the accuracy of the implementations since JSON schema will live closer to the serialization layer that understands how to map .NET types.

One question about the scenario that you've described. AFAIK, required is only describe in the specification for OpenAPI requests (see here). There's no required field for responses (see here). The links I provided where to v3 of the spec but there's no required concept for responses in v3.1 either.

The current design for IOpenApiDocumentTransformer looks like it is making the same mistake as Swashbuckle in that any kind of filter/transform abstraction needs to return its result, otherwise it makes it needlessly difficult and brittle to implement any kind of behavior that replaces the input entirely.

@jcracknell Can you clarify what you mean by this? The current transformer model supports in-place mutations of in-memory model of the OpenAPI document. The link that you shared refers to schemas in particular. Are you referring to the design of schema filters?

I also want to flag that one of the biggest shortcomings of Swashbuckle is that it does not support recursive/re-entrant schema mapping, which makes it impossible to define custom schema mappings for non-trivial types as there is no way to generate or reference the schemas of other types.

With regard to this point, I've referenced the OpenApiComponentService in a few issues. This component is intended to provide APIs for generating schemas and resolving schemas for types already discovered in the document. Full disclosure: schema filters and their associated API are likely to come in a later preview once support for JSON schema lands in STJ.

A quick sketch looks something like this:

Can you share an example of how this API might be consumed for your scenarios?

@jesperkristensen
Copy link

One question about the scenario that you've described. AFAIK, required is only describe in the specification for OpenAPI requests (see here). There's no required field for responses (see here). The links I provided where to v3 of the spec but there's no required concept for responses in v3.1 either.

The required I was referring to is this one: https://spec.openapis.org/oas/v3.0.3#properties

Both requests and responses contain a Media Type Object which contains a Schema Object which contains the required property.

@jcracknell
Copy link

The current design for IOpenApiDocumentTransformer looks like it is making the same mistake as Swashbuckle in that any kind of filter/transform abstraction needs to return its result, otherwise it makes it needlessly difficult and brittle to implement any kind of behavior that replaces the input entirely.

@jcracknell Can you clarify what you mean by this? The current transformer model supports in-place mutations of in-memory model of the OpenAPI document. The link that you shared refers to schemas in particular. Are you referring to the design of schema filters?

@captainsafia currently IOpenApiDocumentTransformer is:

public interface IOpenApiDocumentTransformer {
    public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken);
}

whereas it should likely be:

public interface IOpenApiDocumentTransformer {
    public Task<OpenApiDocument> TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken);
}

Which permits mutation, as well as replacing the document entirely. This is more relevant in the case of filters applying to schemas as replacement is much more likely, but is probably a good rule of thumb. As an example, suppose you want to make a schema nullable. To do this via mutation, you have to (somehow) make an exact copy of the schema, then (somehow) reset the target instance to its empty state, and then finally insert the copied schema and the null schema into the oneOf property of the mutated instance.

When you can return a result on the other hand, this is simply:

return new OpenApiSchema {
    OneOf = {
        schema,
        new OpenApiSchema { Type = "null" }
    }
};

Can you share an example of how this API might be consumed for your scenarios?

Here's an (adapted) version of a highly recursive schema provider for generic trees of criteria that I'm using with a manually patched version of Swashbuckle:

public class CriteriaOpenApiSchemaProvider : IOpenApiSchemaProvider {
    // Slightly clunky, but can be handled in the majority of cases by a base class
    public bool TryGetMapping(Type type, out OpenApiSchemaMapping mapping) {
        if(type.IsGenericType && typeof(Criteria<>) == type.GetGenericTypeDefinition()) {
            mapping = GetSchemaMapping();
            return true;
        } else {
            mapping = default;
            return false;
        }
    }

    private OpenApiSchemaMapping GetSchemaMapping() =>
        // Signals I want to register the result of this mapping as a `$ref` schema
        OpenApiSchemaMapping.Reference(static context => new OpenApiSchema {
            OneOf = {
                new OpenApiSchema {
                    Type = "object",
                    Required = new HashSet<string> { "$and" },
                    Properties = {
                        ["$and"] = new OpenApiSchema {
                            Type = "array",
                            // Generates the `$ref` for this generated schema
                            Items = context.GetSchema(context.Type)
                        }
                    }
                },
                new OpenApiSchema {
                    Type = "object",
                    Required = new HashSet<string> { "$or" },
                    Properties = {
                        ["$or"] = new OpenApiSchema {
                            Type = "array",
                            Items = context.GetSchema(context.Type)
                        }
                    }
                },
                new OpenApiSchema {
                    Type = "object",
                    Required = new HashSet<string> { "$not" },
                    Properties = {
                        ["$not"] = context.GetSchema(context.Type)
                    }
                },
                new OpenApiSchema {
                    Type = "object",
                    Required = new HashSet<string> { "$req" },
                    Properties = {
                        // Generate schema for the type argument, which is unknown
                        ["$req"] = context.GetSchema(context.Type.GetGenericArguments()[0])
                    }
                }
            }
        }); 
}

Here's another mapping for ValueTuples as arrays using OpenAPI 3.1 prefixItems:

OpenApiSchemaMapping.Reference(static context => {
    var elementTypes = GetElementTypes(context.Type).ToList();
    var elementSchemas = elementTypes.Select(context.GetSchema).ToList();

    var prefixItems = new OpenApiArray();
    prefixItems.AddRange(elementSchemas.Select(s => s.ToOpenApiAny()));

    return new OpenApiSchema {
        Type = "array",
        MinItems = elementTypes.Count,
        MaxItems = elementTypes.Count,
        Items = new OpenApiSchema { OneOf = elementSchemas },
        // prefixItems is not supported at our library level, so we can include it as an extension property
        Extensions = new Dictionary<string, IOpenApiExtension> {
            ["prefixItems"] = prefixItems
        }
    };
});

Again, none of this is currently possible in vanilla Swashbuckle, because the only provided means of schema definition is using Func<OpenApiSchema> instances. I mostly want to make sure that this shortfall is covered by your replacement design to align with the existing support for custom STJ JsonConverters.

@bkoelman
Copy link

bkoelman commented Apr 19, 2024

@captainsafia Our open source project enables developers to expose their EF Core models as JSON:API endpoints. It intensively relies on Swashbuckle extensibility, mainly because the signatures of controller action methods are quite different from the produced OpenAPI structure. It's best explained by example.

Consider the following EF Core model:

public class TodoItem
{
    public long Id { get; set;
    public string Description { get; set; } = null!;

    public Person Owner { get; set; } = null!;
    public Person? Assignee { get; set; }
}

public class Person
{
    public long Id { get; set;
    public string? FirstName { get; set; }
    public string LastName { get; set; } = null!;

    public ISet<TodoItem> OwnedTodoItems { get; set; } = new HashSet<TodoItem>();
    public ISet<TodoItem> AssignedTodoItems { get; set; } = new HashSet<TodoItem>();
}

With the following (auto-generated) controller:

public class TodoItemsController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAsync()
    {
        List<TodoItem> entities = GetFromDatabase();
        return Ok(entities)
    }

    [HttpGet("{id}/relationships/{relationshipName}")]
    public async Task<IActionResult> GetRelationshipAsync(long id, string relationshipName)
    {
        // ...
        return Ok(relationship)
    }

    [HttpPost]
    public async Task<IActionResult> PostAsync(TodoItem entity)
    {
        // ...
        return Created(locationUrl, entity);
    }
}

Example response body from GET https://localhost:44340/api/todoItems:

{
  "links": {
    "self": "/api/todoItems",
  },
  "data": [
    {
      "type": "todoItems",
      "id": "1",
      "attributes": {
        "description": "Book vacation"
      },
      "relationships": {
        "owner": {
          "links": {
            "self": "/api/todoItems/1/relationships/owner",
            "related": "/api/todoItems/1/owner"
          }
        },
        "assignee": {
          "links": {
            "self": "/api/todoItems/1/relationships/assignee",
            "related": "/api/todoItems/1/assignee"
          }
        }
      },
      "links": {
        "self": "/api/todoItems/1"
      }
    }
  ]
}

That's quite a different structure. When an API request executes, the structural mismatch is handled by custom implementations for MvcOptions.InputFormatters/OutputFormatters/Conventions that set up routing and transform between JSON and EF Core entities.

Thanks to our customized OpenAPI support, users can generate a C# client using NSwag and write the following code:

TodoItemCollectionResponseDocument response1 = await _apiClient.GetTodoItemCollectionAsync();
Console.WriteLine($"Description: {response1.Data.First().Attributes.Description}");
Console.WriteLine($"AssigneeId: {response1.Data.First().Relationships.Assignee.Data.Id}");

TodoItemIdentifierCollectionResponseDocument response2 =
    await _apiClient.GetPersonOwnedTodoItemsRelationshipAsync("1");
string ownerId = response2.Data.First().Id;

TodoItemPrimaryResponseDocument response3 = await _apiClient.PostTodoItemAsync(new TodoItemPostRequestDocument
{
    Data = new TodoItemDataInPostRequest
    {
        Attributes = new TodoItemAttributesInPostRequest
        {
            Description = "Cook dinner"
        },
        Relationships = new TodoItemRelationshipsInPostRequest
        {
            Owner = new ToOnePersonInRequest
            {
                Data = new PersonIdentifier
                {
                    Type = PersonResourceType.People,
                    Id = ownerId
                }
            }
        }
    }
});

Note how the string relationshipName parameter in the controller action method was expanded to .Relationships.Assignee and Relationships.Owner properties.

To generate an OpenAPI document for the above, the following strategy is used:

  • Define internal generic types to represent the JSON:API objects, for example: AttributesInPostRequest<TEntity>, ToOneRelationshipInResponse<TEntity>
    • Because the OpenAPI required/nullable depends on context (request or response body, post or patch), we need distinct component schemas (so we have distinct generic types for these and other cases, ~40 in total)
  • Register custom IActionDescriptorCollectionProvider/IActionModelConvention that rewrites the FilterDescriptors for parameter and return types to constructed generic types (AttributesInPostRequest<TodoItem>, ToOneRelationshipInResponse<Person>)
    • For the string relationshipName parameter, expand into multiple descriptors, one for each navigation found in DbContext.Model
  • Register a Swashbuckle callback that produces readable component schema IDs, based on the constructed generic types
  • Register a Swashbuckle callback that produces readable operation IDs, based on the constructed generic types
  • Replace the Swashbuckle SchemaGenerator with our own that handles special cases, then delegate to the default SchemaGenerator to produce component schemas for dependent types
    • In JSON:API, sending null means to clear something, whereas not sending it means to leave it unchanged. So we need to handle OpenAPI required/nullable manually, ie [Required] on a property must not translate to non-nullable
    • For the JSON:API type, we generate a single-valued enum, so callers don't need to set a magic string
    • Include/exclude entity properties and relationships, based on configuration (ie: properties can be read-only)
    • Include/exclude various parts (links, free-format metadata, version) based on configuration
    • Custom handling of OpenAPI inheritance with allOf and discriminator that maps to JSON:API type
    • Expand/collapse single-element allOf at various places, to workaround bugs in Swashbuckle
  • Register a custom ISerializerDataContractResolver to:
    • Filter out the "Id" property in the JSON:API attributes object
    • Adapt for different naming conventions
  • Register custom post-processing filters to:
    • Auto-generate documentation (taking triple-slash comments into account) at various nodes in the component schemas
    • Recursively cleanup of unreferenced component schemas (that we had to generate upfront, see remark below)
    • Order endpoints (paths), based on entity and relationship names

What complicates the process is that the built-in SchemaGenerator recursively calls into itself for dependent members and types. Because of that, great care must be taken to ensure the custom generation logic executes in the right order.
For example, data contains type (which needs custom logic), so it must be generated upfront, before calling into the default schema generator for data. It would be much more convenient if there was a factory that enables to return
the appropriate generator, based on the Type to generate a component schema for.

I'm trying to keep it as simple as possible, but there's one more important complication. In some scenarios with relationships, it's impossible to define generic types, because there's no TEntity available to construct it with. In that case, a temporary schema is generated by the default SchemaGenerator, which is then used as a template to produce multiple schemas, and then the temporary schema is deleted from SchemaRepository. This deletion currently requires reflection because the underlying dictionary isn't exposed. It's important to be able to get/add/remove component schemas by both Type and schema ID.

If you'd like to dive deeper, the full source is available at https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/JsonApiDotNetCore.OpenApi.

@benlongo
Copy link

benlongo commented Apr 26, 2024

I'm very happy to see correct handling of polymorphic serialization as a goal :)

Correctly handling polymorphic serialization metadata from STJ in the generated schema using oneOf and type discriminaotrs

By far the biggest issue I've had to date with OpenAPI schema generation (both client side and server side) is discriminated unions. The introduction of [JsonPolymorphic] opens the door for this to be natively handled correctly, and I would love to see a correct generation of a oneOf union with a discriminator that has a mapping for all of the subtypes.
I'm currently implementing this in swashbuckle with a hacky schema filter.

I strongly agree with @jesperkristensen that accuracy and faithfulness to the spec (not the current state of the ecosystem!) should be of focus.

@pinkfloydx33 mentioned the issue I'm alluding to where client generators don't really handle the spec correctly, and we frequently have to tailor the schema to generate the client the way we want (even if the schema is wrong!).

Also please don't force oneOf/allOf for polymorphism/inheritance. While technically correct, some client code generators cannot handle certain combinations of these options. It would be great to at least have an opt-out control here. We've had to specify some counterintuitive options to guarantee accurate client generation. For example nSwag's generator doesn't support oneOf for polymorphism, but does support allOf for inheritance with a type discriminator. When extracting the open api document during CI (where we build clients with nswag) we pass options to the app to ensure oneOf is disabled, but during normal operation (sourcing our swagger-ui) the schemas show up correctly using oneOf.

I'd argue that broken client generators should not be a concern of this implementation. Sufficient escape hatches should be provided (which I believe they are in the current API), but adding corner cases into schema generation to make incorrect client generators behave correctly should definitely be out of scope in my opinion.

This is possibly out-of-scope, but I've run into it repeatedly over the years so figured it's worth a mention: the semantics of structurally optional properties vs. required nullable properties is not easy to express in C#/STJ (natively at least).

For example, let's say we want to implement a PATCH endpoint on an entity Foo that has a string? Name { get; set; } property. One might attempt to implement this with a nullable string on the patch payload:

public record FooPatchDTO {
    public string? Name { get; init; }
}

The ideal contract (in my opinion), is that { "name": null } means update name to have null value and {} means don't touch Name. In order to deal with this, I've always introduced a new type named something like Optional<T> or Patch<T> to enable the consumer to tell the difference.

This is again something that required a hacky schema filter to get working properly. It would be wonderful if I could get a type like that to work with the upcoming native implementation of JSON schema via STJ contracts, but I'm not sure if that will be possible. I've asked about this here dotnet/runtime#100159 (comment) but I'm not sure if that's the right place.

On a related note, it would be wonderful if the recently introduced required modifier is handled correctly.

@captainsafia
Copy link
Member Author

Hello folks --

Some updates on this work as the code-complete deadline for preivew4 has passed. Here's what's landing in preview4:

  • Support for generating OpenAPI document
  • Support for modifying document via document and operation transformers
  • Support for generating inline schemas using a prototype of the JSON schema implementation coming up in STJ

The last point is ab it interesting so I'll elaborate on it here. Originally prototypes of this work involved implementing an OpenAPI-specific schema generator as part of Microsoft.AspNetCore.OpenApi. For .NET 9, we expect that JSON schema generation support will land in the System.Text.Json library (dotnet/runtime#100159). The implementation of JSON schema in this OpenAPI support will build on top of this.

There are some distinctions between the schema specs supported by OpenAPI and those supported by JSON schema. To resolve this, we're using a combination of callback APIs + shim types to mutate the schema generated by STJ to match the supserset expected by OpenAPI schema.

I expect that the schema implementation will evolve a lot over the coming previews. There's some gaps in preview4, like the fact that we don't manage schemas by reference or support schema transformers, that need to be addressed. And of course, we'll have to react once the STJ schema prototypes officially land in the runtime.

With that said, some answers to the questions that have popped up in this thread while I was slinging code 😅 ...

Which permits mutation, as well as replacing the document entirely. This is more relevant in the case of filters applying to schemas as replacement is much more likely, but is probably a good rule of thumb.

@jcracknell One thing to note about the schema transformers that are described in #54650 is that they don't function on the document like operation/document transformers. They mutate the schemas before they are committed to the document.

Here's an (adapted) version of a highly recursive schema provider for generic trees of criteria that I'm using with a manually patched version of Swashbuckle:

The IOpenApiSchemaProvider interface you've mentioned in this code sample is similar to the OpenApiComponentService that the implementation currently uses to generate schemas. The API for this interface isn't publicly yet but I anticipate that this is the place where will provide a way to augment the list of schemas to be referenced in the implementation.

But I think the crux of your issue is that you want to be able to modify schemas on a more global level as opposed to filters/transformers which operate on one schema at a time.

@captainsafia Our open source project enables developers to expose their EF Core models as JSON:API endpoints. It intensively relies on Swashbuckle extensibility, mainly because the signatures of controller action methods are quite different from the produced OpenAPI structure. It's best explained by example.

Cool project! I took a peek at some of the code and have some thoughts:

Some of the API surface area that we have proposed (document/operation transformers) will exist in the built-in implementation.

Some aspects of your current implementation (like overriding the ISchemaGenerator implementation) are not likely to be feasible in the same shape, especially because we build on top of System.Text.Json's schema generator. I think that both STJ's schema generator callbacks and our transformer APIs will allow you to achieve something close here. For scenarios where you are implementing a custom schema generator to fix underlying bugs, those will hopefully be handled by default in the standard implementation.

Modifying OperationIds is something I expect to be achieved via operation transformers as opposed to the individual Action provided in options that OpenAPI supports.

By far the biggest issue I've had to date with OpenAPI schema generation (both client side and server side) is discriminated unions. The introduction of [JsonPolymorphic] opens the door for this to be natively handled correctly, and I would love to see a correct generation of a oneOf union with a discriminator that has a mapping for all of the subtypes.
I'm currently implementing this in swashbuckle with a hacky schema filter.

@benlongo Yep. WRT to how we handle this, we take the schema using oneOf that the STJ produces and map it to the discriminator + mapping nomenclature that OpenAPI schema uses. Note: this doesn't work 100% as of preview4 because we don't have the support for schemas-by-reference that the mapping property in OpenAPI depends on but I hope to land this in preview5.

I'd argue that broken client generators should not be a concern of this implementation. Sufficient escape hatches should be provided (which I believe they are in the current API), but adding corner cases into schema generation to make incorrect client generators behave correctly should definitely be out of scope in my opinion.

I agree with this sentiment and I believe the fact that we build on-top of the schema generator in STJ pushes us to be spec compliant, especially when we think about the long-term health of this work as the OpenAPI and JSON schema specs evolve (e.g OpenAPI v3.1 and OpenAPI v4/Moonwalk).

Document/operation transformers, schema transformers, and schema generation callbacks should provide enough API to modify the default behavior for certain tools.

The ideal contract (in my opinion), is that { "name": null } means update name to have null value and {} means don't touch Name. In order to deal with this, I've always introduced a new type named something like Optional or Patch to enable the consumer to tell the difference.

I believe the correct representation for this as far as OpenAPI schema is concerned is something to the effect of:

{
  "type": "object",
  "properties": {
    "name": {
	 "required": false,
      "type": "string",
	  "nullable": true
    }
  }
}

This is again something that required a hacky schema filter to get working properly. It would be wonderful if I could get a type like that to work with the upcoming native implementation of JSON schema via STJ contracts, but I'm not sure if that will be possible. I've asked about this here dotnet/runtime#100159 (comment) but I'm not sure if that's the right place.

I think this is the right place to post. I'll catch up on that thread in a bit. FWIW, I think we landed on a good API for the mutation callbacks supported by STJ is robust enough (see here).

On a related note, it would be wonderful if the recently introduced required modifier is handled correctly.

I believe this is the case but I'll double check with additional test coverage.

Thanks for getting to the end of this long comment! 😅 Looking forward to polishing up this space with you all. I'll be spending this week focusing on a docs revamps for OpenAPI.

@jcracknell
Copy link

@captainsafia

One thing to note about the schema transformers that are described in #54650 is that they don't function on the document like operation/document transformers. They mutate the schemas before they are committed to the document.

I mean I guess the flag I'm raising is that there are scenarios where you want to replace the subject value entirely. Given that JSON schemas are both highly composable and difficult to inspect in the presence of other unknown filter behaviors, it seems odd to adopt a fllter design that makes compositional operations difficult. I have Swashbuckle schema filters in production which use reflective fieldwise copies for this purpose. Obviously this could be set using the filter context object if the thinking is that returning the filter output is potentially confusing.

The IOpenApiSchemaProvider interface you've mentioned in this code sample is similar to the OpenApiComponentService that the implementation currently uses to generate schemas.

Yes, these appear to be approximate analogs. I'm mostly noting that the JsonConverter analog for schemas needs to provide both a disposition (inline or reference) as well as a schema factory in order to support recursion.

@Varorbc
Copy link
Contributor

Varorbc commented Apr 30, 2024

Without the Swagger ui, the experience would be bad.

@Chrille79
Copy link

Chrille79 commented May 7, 2024

Without the Swagger ui, the experience would be bad.

Look at the sample project. If its not in the framework, why not put it besides?

https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/sample/EndpointRouteBuilderExtensions.cs

@martincostello
Copy link
Member

In that sample it's just pointing to whatever the latest version of swagger-ui is, rather than using a specific version, so it could theoretically break at any time with a new major version. Fixing that to use a pinned version then means you need a process to update it if there's a vulnerability, and now suddenly there's a lot more complexity.

@Chrille79
Copy link

In that sample it's just pointing to whatever the latest version of swagger-ui is, rather than using a specific version, so it could theoretically break at any time with a new major version. Fixing that to use a pinned version then means you need a process to update it if there's a vulnerability, and now suddenly there's a lot more complexity.

If you add that extension to your project, you can change to run a specific version and probably a downloaded version instead of a linked one

@martincostello
Copy link
Member

Exactly - if you copy it, you're in complete control and can do whatever you want. Adding it to the framework adds additional complexity regarding versioning and security, which is why it's not as simple as just copy-pasting that sample method into an assembly and making it public.

@Chrille79
Copy link

Exactly - if you copy it, you're in complete control and can do whatever you want. Adding it to the framework adds additional complexity regarding versioning and security, which is why it's not as simple as just copy-pasting that sample method into an assembly and making it public.

Then I think we are on the same page.
It doesn't belong in the framework!
If you want the full experience, it is not more difficult to take the code in the sample, modify it to your own requirements and make it fly

@martincostello
Copy link
Member

I must have misunderstood what you mean by this then 😄

If its not in the framework, why not put it

@JohnGalt1717
Copy link

The biggest issue here, and I'd like to see @davidfowl chime in is:

With Aspire, the dependencies that are injected for any given micro service are spun up by host on dev. (and NEVER on CI/CD where this is already a massive problem) As a result, your micro service CANNOT independently run successfully because it's dependencies won't be there (i.e. Redis cache has to ping, RabbitMQ won't be there, DataProtection will execute and blow up because the database isn't there.

Hence, the strategy, which was never the right approach and just forced because of how the documenter worked of running the project, actually spinning it up, and then reflecting on the running solution, is no longer viable in the day and age of Aspire. (it never was, but now it's really bad)

This needs a source generator and the effect of that generator is the swagger.

It should be looking for the minimal apis, controllers, or anything else that is documented (i.e. see FastEndpoints/FastEndpoints#677 for Fast-Endpoints) appropriately and then walking the stack of dependent DTOs etc. from the endpoint implementations without having to execute the code.

Anything else is just going to have to be rebuilt anyhow because of the Aspire problem IMHO. Add in that actually executing your web service during CI/CD and making sure that it has connection strings, and can actually access dependencies like listed above, was always a PITA that needs to go away.

Same goes for KOTA clients.

@captainsafia
Copy link
Member Author

@JohnGalt1717 Some thoughts on this based on prototyping and thinking I've done in the past.

An OpenAPI source generator is an appealing option but there's a few challenges with it:

  • Given our current capabilities, we'd only be able to support OpenAPI generation for minimal APIs, at minimum. There's prior art for being able to statically analyze a minimal API and create a in-memory model that we can use to generate the OpenAPI document at build-time. We don't have prior art for static analysis on controller-based APIs so there's an unknown to resolve here.
  • There are aspects of the application that influence the OpenAPI model that are really hard to statically analyze at runtime. Input/output formatters in MVC, implementers of the IEndpointMetadataProvider interface, etc. are really hard to statically analyzer and introspect.
  • At the moment, source generators don't support producing anything but C# source files. There's hacky workarounds you can use to produce non-C# files but they're not viable strategies for something we'd ship in framework. There's an open proposal to add capabilities to emit non-C# files from generators but until that is done you get a host of wacky emission strategies.
  • With a runtime-based implementation, we can build on the capabilities of the IApiDescriptionProvider to allow any web framework built on-top of ASP.NET to light up OpenAPI support. There's overhead to consider what kinds of abstractions a source generator based approach would provide to allow web frameworks in the ecosystem to opt-in. Is it up to each framework to roll its own generator? Do the frameworks implement a generator that writes some intermediary state that ASP.NET Core's OpenAPI generator would consume? There's design thinking to do here.

So, those are the challenges that make a purely source-generator based implementation for OpenAPI difficult to implement.

Now, you've mentioned a totally valid problem with the current approach (for both Aspirified and non-Aspirief implementations), our current approach always requires you to run the application in order to resolve the OpenAPI file. For local development scenarios, this is tangible. But it gets complicated in CI when you have dependencies that might not be configured. People end up having to configure their application startup with special configuration that stubs out uninitialized dependencies when called from the build-time generation problem. Source generators are one approach to solve this problem, but there are likely alternatives that are less expensive for solving this problem.

@julealgon
Copy link

People end up having to configure their application startup with special configuration that stubs out uninitialized dependencies when called from the build-time generation problem. Source generators are one approach to solve this problem, but there are likely alternatives that are less expensive for solving this problem.

@captainsafia isn't this similar to the problem that the EFCore team faces with the need to run migrations (and then have to load assemblies with potential configuration) to do that?

I recall at one point a strategy was devised where people could implement a custom "factory" method that would only be called by the migration mechanism to load the needed dbcontext without the rest of the dependencies configured. They called this "design-time factory".

This later gave rise to the idea of "migration bundles" which are an even more decoupled approach to apply migrations.

Perhaps something similar could be devised for OpenAPI generation, that would only look at API-related aspects of the application without loading all of its external dependencies and then produce the OpenAPI spec document.

I can say it is an absolute PITA to have to run your full API app just to grab the openapi document from it at build-time... we need that for updating our Azure API Management schemas during CI/CD for each of our APIs hosted there. A mechanism similar to EFCore bundles (or even the "design-time factory" approach) would be very welcome here.

@Eneuman
Copy link

Eneuman commented May 16, 2024

I can only agree, it's such a pita having to start a the app just to create the json. We have lots of code just to disable different things like Application Insight Profiler because it can't run when creating the json

@JohnGalt1717
Copy link

JohnGalt1717 commented May 16, 2024

People end up having to configure their application startup with special configuration that stubs out uninitialized dependencies when called from the build-time generation problem. Source generators are one approach to solve this problem, but there are likely alternatives that are less expensive for solving this problem.

@captainsafia isn't this similar to the problem that the EFCore team faces with the need to run migrations (and then have to load assemblies with potential configuration) to do that?

I recall at one point a strategy was devised where people could implement a custom "factory" method that would only be called by the migration mechanism to load the needed dbcontext without the rest of the dependencies configured. They called this "design-time factory".

This later gave rise to the idea of "migration bundles" which are an even more decoupled approach to apply migrations.

Perhaps something similar could be devised for OpenAPI generation, that would only look at API-related aspects of the application without loading all of its external dependencies and then produce the OpenAPI spec document.

I can say it is an absolute PITA to have to run your full API app just to grab the openapi document from it at build-time... we need that for updating our Azure API Management schemas during CI/CD for each of our APIs hosted there. A mechanism similar to EFCore bundles (or even the "design-time factory" approach) would be very welcome here.

Swashbuckle has this. It is great in theory but in practice, you end up with it breaking all of the time on top of the fact it's a black box that can't be debugged.

PS: EF Core is a little different in that they need the connection created so that they can interogate the state of the database to create the migration. That isn't required here.

@JohnGalt1717
Copy link

@JohnGalt1717 Some thoughts on this based on prototyping and thinking I've done in the past.

An OpenAPI source generator is an appealing option but there's a few challenges with it:

  • Given our current capabilities, we'd only be able to support OpenAPI generation for minimal APIs, at minimum. There's prior art for being able to statically analyze a minimal API and create a in-memory model that we can use to generate the OpenAPI document at build-time. We don't have prior art for static analysis on controller-based APIs so there's an unknown to resolve here.
  • There are aspects of the application that influence the OpenAPI model that are really hard to statically analyze at runtime. Input/output formatters in MVC, implementers of the IEndpointMetadataProvider interface, etc. are really hard to statically analyzer and introspect.
  • At the moment, source generators don't support producing anything but C# source files. There's hacky workarounds you can use to produce non-C# files but they're not viable strategies for something we'd ship in framework. There's an open proposal to add capabilities to emit non-C# files from generators but until that is done you get a host of wacky emission strategies.
  • With a runtime-based implementation, we can build on the capabilities of the IApiDescriptionProvider to allow any web framework built on-top of ASP.NET to light up OpenAPI support. There's overhead to consider what kinds of abstractions a source generator based approach would provide to allow web frameworks in the ecosystem to opt-in. Is it up to each framework to roll its own generator? Do the frameworks implement a generator that writes some intermediary state that ASP.NET Core's OpenAPI generator would consume? There's design thinking to do here.

So, those are the challenges that make a purely source-generator based implementation for OpenAPI difficult to implement.

Now, you've mentioned a totally valid problem with the current approach (for both Aspirified and non-Aspirief implementations), our current approach always requires you to run the application in order to resolve the OpenAPI file. For local development scenarios, this is tangible. But it gets complicated in CI when you have dependencies that might not be configured. People end up having to configure their application startup with special configuration that stubs out uninitialized dependencies when called from the build-time generation problem. Source generators are one approach to solve this problem, but there are likely alternatives that are less expensive for solving this problem.

All valid.

So what I've done, because I got so fed up with the state of swagger in the past, was I wrote a CLI that you pointed at the result of the compile stage. It would load the assembly graph, reflect on that finding all endpoints from that reflection, and then generate clients directly that were done the way I wanted them.

Could this approach be done while we're waiting to convert that to a source generator? A dotnet tool that takes as the input the dll/exe and reflects on it and generates the openAPI like the source generator would create?

That way you could easily just create a tasks.json execution for it, and you could easily CI/CD it with the tool that would generate an artifact in the destination folder for the openapi.json file and it wouldn't require that it be run.

One would think that using the attributes on the stuff being reflected, that that would also be straight forward for controllers since a controller that takes more than JSON should be adorned with the output types that it generates. Just make that a requirement at at least the class level to define anything more than JSON as output and input types, and you solve that issue in the reflective model.

If built carefully, the KOTA team could also make use of the same work to generate clients in other languages directly instead of requiring the same steps one would think.

Only thing I can think of is that AOT might defeat this, but perhaps there's an easy why to leave the unshaken version around that still has the metadata needed? Is this even an issue really?

As an aside, please, whatever you generate, please generate OpenAPI 3.1 with proper enums with key/values and generics correctly too.

@Eneuman
Copy link

Eneuman commented May 16, 2024

It would be great if we could get a easy way to check if the app is running because it want to generate a OpenAI json. I have seen that people are using different way and they all look kinda messy :(

@gregsdennis
Copy link

At the moment, source generators don't support producing anything but C# source files.

Sounds like a reason to go API-first, to me. Design the API with your OpenAPI doc and let a source generator build the code for it.

Thanks for coming to my TED talk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates Epic Groups multiple user stories. Can be grouped under a theme. feature-openapi
Projects
None yet
Development

No branches or pull requests