-
Notifications
You must be signed in to change notification settings - Fork 10.3k
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
Conversation
There was a problem hiding this 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
src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Martin Costello <[email protected]> Co-authored-by: Rick Anderson <[email protected]>
src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
var documentService = new OpenApiDocumentService(hostEnvironment); | ||
var serviceCollection = new ServiceCollection(); | ||
serviceCollection | ||
.AddOpenApi("v2") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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");
There was a problem hiding this comment.
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. 😄
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/OpenApi/src/Extensions/OpenApiServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
var documentService = context.RequestServices.GetRequiredKeyedService<OpenApiDocumentService>(documentName); | ||
var document = await documentService.GetOpenApiDocumentAsync(); | ||
var documentOptions = options.Get(documentName); | ||
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
context.Response.StatusCode = StatusCodes.Status200OK; | ||
context.Response.ContentType = "application/json;charset=utf-8"; | ||
await context.Response.WriteAsync(stringWriter.ToString(), context.RequestAborted); |
There was a problem hiding this comment.
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?
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") |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
I'n going to go ahead and merge this for now. Follow-ups include:
|
* 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]>
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)
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.