Skip to content

Add entry-point APIs for OpenAPI support #54789

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

Merged
merged 7 commits into from
Mar 29, 2024

Conversation

captainsafia
Copy link
Member

@captainsafia captainsafia commented Mar 26, 2024

This PR adds the set of entry-point APIs needed for build-in OpenAPI document generation to the framework.

The included APIs are tracked in the following issues:

Note: this PR intentionally lacks any functionality for generating the actual document. This will be included in follow-up PRs to the feature branch.

@captainsafia captainsafia requested review from a team and wtgodbe as code owners March 26, 2024 22:23
@dotnet-issue-labeler dotnet-issue-labeler bot added the old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels label Mar 26, 2024
@captainsafia captainsafia added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels labels Mar 26, 2024
Copy link
Contributor

@Rick-Anderson Rick-Anderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tdykstra please review my review

var documentService = new OpenApiDocumentService(hostEnvironment);
var serviceCollection = new ServiceCollection();
serviceCollection
.AddOpenApi("v2")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if I call .AddOpenApi("foo").AddOpenApi("foo")? Is it worth testing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're used keyed DI under the hood so the last registration is used. For the exact scenario you presented, you'll get virtually the same behavior. If you did something like this though:

services
	.AddOpenApi("v1")
	.AddOpenApi("v1", options => options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0);

Then you'll get a document serialized using the OpenApi v2 since the last registration is favored.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure you'll get the document serialized using OpenApio v2 in the given the opposite order too, right?

services
	.AddOpenApi("v1", options => options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0)
	.AddOpenApi("v1");

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like we need a test. 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@halter73 Yep, you're right. The test I added here didn't cover this scenario since I was overriding the value of OpenApiVersion in the second call.

Digging deeper into the Options code, it looks like each registered IConfigureOptions instance is called for the same named option (ref), so what you effectively get is a union of all the configured options.

Perhaps this is "fine" behavior. Thoughts?

Copy link
Member

@amcasey amcasey Mar 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason you might want to add a document twice is to modify the configuration of a document added elsewhere in the code? i.e. there's a reason to not just throw when this happens (which I'm sure is unidiomatic in aspnetcore)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason you might want to add a document twice is to modify the configuration of a document added elsewhere in the code?

That would be one reason yes. In which case, the fact that configurations get unioned would make sense. My suspicion is that users will likely only be calling AddOpenApi once per document in their code.

i.e. there's a reason to not just throw when this happens (which I'm sure is unidiomatic in aspnetcore)?

Throwing is an option, but feels a little aggressive, assuming the default behavior is well-documented and sane enough.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I jotted this as an implementation consideration and will solicit feedback on the API issue associated with this.

var documentService = context.RequestServices.GetRequiredKeyedService<OpenApiDocumentService>(documentName);
var document = await documentService.GetOpenApiDocumentAsync();
var documentOptions = options.Get(documentName);
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems allocatey. What about using SignalR's Utf8BufferTextWriter with the response PipeWriter instead? It will buffer indefinitely without flushing, so blocking shouldn't be a concern.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good pointer to Utf8BufferTextWriter. I think I was able to wire this up correctly with the in-memory buffer writer.

// It would be ideal to use the `HttpResponseStreamWriter` to
// asynchronously write to the response stream here but Microsoft.OpenApi
// does not yet support async APIs on their writers.
// See https://github.com/microsoft/OpenAPI.NET/issues/421 for more info.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we pushed to get this resolved?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not aggressively -- it's on deck for their v2.1 release. I think if we're able to work around this with the buffered writer we should be fine.

var documentOptions = options.Get(documentName);
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
var jsonWriter = new OpenApiJsonWriter(stringWriter);
document.Serialize(jsonWriter, documentOptions.OpenApiVersion);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing we could consider is caching the response for each document in a byte array. If we do that, the impact of this becomes smaller because all the concurrent requests for a given document would share a buffer. We could lean on output caching middleware for this, but I don't think that should be necessary.

Comment on lines 51 to 53
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.ContentType = "application/json;charset=utf-8";
await context.Response.WriteAsync(stringWriter.ToString(), context.RequestAborted);
Copy link
Member

@halter73 halter73 Mar 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use TypedResults.Text() for both the 200 and 404 responses if Utf8BufferTextWriter doesn't work out?

Suggested change
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.ContentType = "application/json;charset=utf-8";
await context.Response.WriteAsync(stringWriter.ToString(), context.RequestAborted);
return TypedResults.Text(stringWriter.ToString(), "application/json;charset=utf-8");

That way you could remove the HttpContext parameter and inject the IServiceProvider instead.

var documentService = new OpenApiDocumentService(hostEnvironment);
var serviceCollection = new ServiceCollection();
serviceCollection
.AddOpenApi("v2")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure you'll get the document serialized using OpenApio v2 in the given the opposite order too, right?

services
	.AddOpenApi("v1", options => options.OpenApiVersion = OpenApiSpecVersion.OpenApi2_0)
	.AddOpenApi("v1");

context.Response.Body = responseBodyStream;
context.RequestServices = serviceProvider;
context.Request.RouteValues.Add("documentName", "v2");
var endpoint = builder.DataSources.First().Endpoints.First();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be easier in the long run to convert these to use TestServer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect us to add too many additional tests to this particular codepath after this PR. I have an alternate strategy for validating the contents of the OpenAPI document that actually gets generated.

@captainsafia
Copy link
Member Author

I'n going to go ahead and merge this for now. Follow-ups include:

  • Close on the best way to handle duplicate AddOpenApi calls
  • Verify implementation with Utf8BufferedTextWriter in endpoint

@captainsafia captainsafia merged commit b082c5f into feature/openapi Mar 29, 2024
19 of 21 checks passed
@captainsafia captainsafia deleted the safia/api-scaffold branch March 29, 2024 20:07
captainsafia added a commit that referenced this pull request Apr 2, 2024
* Add entry-point APIs for OpenAPI support

* Apply suggestions from code review

Co-authored-by: Martin Costello <[email protected]>
Co-authored-by: Rick Anderson <[email protected]>

* Update docs and pass cancellation token to WriteAsync

* Address feedback

* Remove trailing comma in slnf

* Address more feedback

* Seal OpenApiOptions

---------

Co-authored-by: Martin Costello <[email protected]>
Co-authored-by: Rick Anderson <[email protected]>
captainsafia added a commit that referenced this pull request Apr 18, 2024
Co-authored-by: Martin Costello <[email protected]>
Co-authored-by: Rick Anderson <[email protected]>

This PR adds support for OpenAPI document generation, sans schema generation to Microsoft.AspNetCore.OpenApi. Relevant changes are available in individual PRs:

- Add entry-point APIs for OpenAPI support (#54789)
- Support resolving OpenApiPaths entries from document (#54847) 
- Support generating OpenAPI operation and associated fields (#54903) 
- Add APIs for OpenAPI document transformers (#54935) 
- Add support for generating OpenAPI parameters (#55041)
- Add support for generating OpenAPI responses (#55020) 
- Add support for generating OpenAPI request bodies (#55040)
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 feature-openapi
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants