aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Add built-in support for generating OpenAPI document from APIs

Open captainsafia opened this issue 11 months ago • 71 comments

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:

  • [x] Correct handling duplicate FromForm parameters in a given API (ref)
  • [x] Correct handling nullability annotations for response types (ref)
  • [x] Correctly handling IFormFile and IFormFileCollection inputs
  • [x] Correctly handling polymorphic serialization metadata from STJ in the generated schema using oneOf and type discriminaotrs
  • [x] Correctly handling inheritance hierarchies in types using allOf
  • [x] Applying all supported route constraints to generated schemas
  • [x] 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 https://github.com/dotnet/aspnetcore/issues/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

preview4

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

preview5

  • [x] ~~Replat WithOpenApi on top of operation transformers~~
  • [x] Update web API templates to use new APIs
  • [x] Verify native AoT comapt for minimal APIs scenarios

preview6

  • [x] Add support for schema transformers
  • [x] Add support for reference IDs and schema reference

preview7

  • [ ] Support integrating XML comments into generated code
  • [x] Support configuring discriminator mappings on polymorphic types
  • [ ] Update gRPC JSON transcoding implementation

P1: Ecosystem Integration and Enhancements

  • [ ] Support incorporating API versions from Asp.Versioning
  • [ ] Validate generation experience with Kiota, NSwag

captainsafia avatar Mar 18 '24 14:03 captainsafia

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.

cphorton avatar Mar 18 '24 19:03 cphorton

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.

captainsafia avatar Mar 18 '24 19:03 captainsafia

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

cphorton avatar Mar 18 '24 20:03 cphorton

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 avatar Mar 18 '24 20:03 captainsafia

.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)

RicoSuter avatar Mar 18 '24 22:03 RicoSuter

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

Ooops. That should've been a link to https://github.com/dotnet/runtime/issues/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.

captainsafia avatar Mar 18 '24 22:03 captainsafia

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).

RicoSuter avatar Mar 18 '24 22:03 RicoSuter

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.

captainsafia avatar Mar 18 '24 22:03 captainsafia

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.

swythan avatar Mar 18 '24 23:03 swythan

@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.

captainsafia avatar Mar 18 '24 23:03 captainsafia

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.)

desjoerd avatar Mar 18 '24 23:03 desjoerd

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.

captainsafia avatar Mar 19 '24 00:03 captainsafia

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 avatar Mar 19 '24 00:03 desjoerd

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

desjoerd avatar Mar 19 '24 00:03 desjoerd

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 avatar Mar 19 '24 06:03 Lanayx

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.

jimcward avatar Mar 19 '24 15:03 jimcward

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 avatar Mar 19 '24 16:03 captainsafia

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.

captainsafia avatar Mar 19 '24 16:03 captainsafia

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!

jimcward avatar Mar 19 '24 17:03 jimcward

Another use case I've often used the schema filter extensibility for is to add example requests/responses for consumption in Swagger UI.

martincostello avatar Mar 19 '24 17:03 martincostello

Another use case I've often used the schema filter extensibility for is to add example requests/responses for consumption in Swagger UI.

Yeah, there's only ever going to be so much you can do "out of the box" - for me, that's the great thing about SwashBuckle - those extension points that allow you to tailor the output to exactly what you need :) For operations + schemas especially, having some kind of hook where the library tells you "ok, here's what I've generated for you and here is the context I based it on" and then letting you manipulate it is incredibly useful.

jimcward avatar Mar 19 '24 17:03 jimcward

@captainsafia the tool is called "kiota", not "kioata". You might want to update your references in this issue here just for search purposes.

I do appreciate how you made sure to mention it as a first class thing, though. That was exactly what I was looking forward to (and why I even found out the name divergence there... since I actually searched directly for "kiota" on the browser 😅)

Additionally, this could be an opportunity to integrate with the Visual Studio team on this front here:

I opened this suggestion somewhat recently after first trying kiota out in one of our integrations.

julealgon avatar Mar 19 '24 18:03 julealgon

@captainsafia the tool is called "kiota", not "kioata". You might want to update your references in this issue here just for search purposes.

🤦🏽‍♀️ 😅 Thanks! I've been misspelling that name since the thing was in preview. No chance I'm gonna figure it out now. But I updated the issue text for better discoverability.

I do appreciate how you made sure to mention it as a first class thing, though.

Yep, to be clear about this, we're not planning on building first-class integrations with the things mentioned here. It's more about making sure that you can export the OpenAPI document we produce, plug it into the generation tools mentioned and be able to get sensible clients out of it. The list I identified is stuff we'll want to test/document integrations with.

For operations + schemas especially, having some kind of hook where the library tells you "ok, here's what I've generated for you and here is the context I based it on" and then letting you manipulate it is incredibly useful.

@jimcward Yep -- totally agree. Being able to customize the generated document is also helpful from a framework perspective because it gives users an escape hatch in case the default generation behavior isn't to their liking. Getting a sense of what modifications people are make gives me an idea of the context that needs to be present in order to make modifications. I'm leaning towards reusing the existing ApiExplorer types here and not introducing any new context types since the ApiExplorer types are pretty rich.

captainsafia avatar Mar 19 '24 18:03 captainsafia

I implemented the nullable reference type and generic nullables handling in Swashbuckle and one of that hardest parts was to figure out if a type could be null or not because of the current CLR optimisations and the lack of helper methods. A lot of reflection was needed that I think will block AOT compilation if implemented the same way in this PR.

Just my two cents.

Ref: https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md

Eneuman avatar Mar 19 '24 20:03 Eneuman

@captainsafia I'm actually talking about this line, F# frameworks (Giraffe, Oxpecker, Falco) all work using RequestDelegate handlers, this enables flexibility in composing/decomposing it into subhandlers, but this also eliminates ability of inferring types and thus generating OpenApi spec. What I'm saying is that there should exist some explicit way to pass input/output types to metadata, so they can be used for OpenApi needs.

Lanayx avatar Mar 19 '24 20:03 Lanayx

I implemented the nullable reference type and generic nullables handling in Swashbuckle and one of that hardest parts was to figure out if a type could be null or not because of the current CLR optimisations and the lack of helper methods. A lot of reflection was needed that I think will block AOT compilation if implemented the same way in this PR.

Just my two cents.

Ref: https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md

Why would AOT be a problem? Will it remove the compiler generated nullable attributes?

RicoSuter avatar Mar 19 '24 21:03 RicoSuter

@RicoSuter Reflection isn't supported in Native AOT.

Without this added code, System.Text.Json uses reflection to serialize and deserialize JSON. Reflection isn't supported in Native AOT.

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0

But maby there is away around this?

There is a post in clr repo discussing implementing a helper method, I'll see if I can find it.

Eneuman avatar Mar 19 '24 21:03 Eneuman

What I'm saying is that there should exist some explicit way to pass input/output types to metadata, so they can be used for OpenApi needs.

If we remove the short-circuiting for RequestDelegates, we could make this possible via WithOpenApi's callback. However, since we cannot really infer much from a RequestDelegate anyway, it might be better to just create your own OpenApiOperation and add it to the metadata as follows.

app.MapGet("/get-json-array", context => context.Response.WriteAsJsonAsync<int[]>([1, 2, 3]))
    .WithMetadata(new OpenApiOperation
    {
        Responses = new OpenApiResponses
        {
            ["200"] = new OpenApiResponse
            {
                Description = "OK",
                Content =
                {
                    ["application/json"] = new OpenApiMediaType
                    {
                        Schema = new OpenApiSchema
                        {
                            Type = "array",
                            Items = new OpenApiSchema
                            {
                                Type = "int"
                            }
                        }
                    }
                }
            }
        }
    });

Or, if you wanted to get the metadata you would have gotten for a given MethodInfo for your RequestDelegate, you could add it yourself:

app.MapGet("/get-json-array", context => context.Response.WriteAsJsonAsync<int[]>([1, 2, 3]))
    .WithMetadata(typeof(Func<int[]>).GetMethod("Invoke")!)
    .WithOpenApi();

halter73 avatar Mar 20 '24 00:03 halter73

We do a lot of customization via Swashbuckle filters at the document/operation/schema levels. We have filters that recognize the method of invocation (either a direct/internal request or one coming through our api gateway portal and thus a request for an external api doc) and adjust header names, operation paths, etc. to reflect internal vs external consumption and usage patterns. We also use filters to automatically add required roles/permissions to the operation descriptions, pagination related headers to the response, vendor specific annotations (ie. for Nswag generators), schema customization for file downloads, custom schema names, schema customization for various types (ie. dictionary where key=guid), recognition of custom internal binding sources and much, much more.

It'd be unfortunate to lose all of these customization points and would be a reason we'd have to stick with Swashbuckle.

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.

pinkfloydx33 avatar Mar 20 '24 01:03 pinkfloydx33

@halter73 Thanks for the hint, I'll give it a try!

Lanayx avatar Mar 20 '24 02:03 Lanayx