-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Add support for generating OpenAPI request bodies #55040
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
if (bodyParameters.Count() == 1) | ||
{ | ||
bodyParameter = bodyParameters.Single(); |
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.
Use SingleOrDefault()
?
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 think that throws if there are multiple elements? (Unless you meant under the if, in which case, why?)
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 would have gone the other way and suggested First
since there's no reason to do another bounds check.
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.
Oh that's me misremembering then. I thought Single() was one or exception, and SingleOrDefault() was one or null/default.
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.
SingleOrDefault
will only return null/default if the list is empty. If there are multiple elements, it'll through an exception. Although based on the check that we are doing in the if, that should never be the case hence my inclination to use Single
. First
could also be used here but I feel it obfuscates the intention that there should always be only one parameter resolved from the JSON request body in a given request.
return prefix + "AnonymousType"; | ||
} | ||
|
||
return prefix + type.Name.Split('`').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.
(Teeny) perf vs readability option:
return prefix + type.Name.Split('`').First(); | |
return prefix + type.Name.Split('`')[0]; |
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.
That's not the thing that's going to slow this code down. 😆
if (bodyParameters.Count() == 1) | ||
{ | ||
bodyParameter = bodyParameters.Single(); |
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 think that throws if there are multiple elements? (Unless you meant under the if, in which case, why?)
if (bodyParameters.Count() == 1) | ||
{ | ||
bodyParameter = bodyParameters.Single(); |
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 would have gone the other way and suggested First
since there's no reason to do another bounds check.
{ | ||
if (!type.IsConstructedGenericType) | ||
{ | ||
return type.Name.Replace("[]", "Array"); |
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.
typeof(int[,])
/// </summary> | ||
/// <param name="type">The <see cref="Type"/> to resolve a schema reference identifier for.</param> | ||
/// <returns>The schema reference identifier associated with <paramref name="type"/>.</returns> | ||
public static string GetSchemaReferenceId(this Type type) |
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.
Can type
be a nested type?
} | ||
|
||
var prefix = type.GetGenericArguments() | ||
.Select(GetSchemaReferenceId) |
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'd be a little nervous about blowing the stack here.
public static bool IsAnonymousType(this Type type) => | ||
type.GetTypeInfo().IsClass | ||
&& type.GetTypeInfo().IsDefined(typeof(CompilerGeneratedAttribute)) | ||
&& !type.IsNested |
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.
Why can't anonymous types be nested?
return prefix + "AnonymousType"; | ||
} | ||
|
||
return prefix + type.Name.Split('`').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.
That's not the thing that's going to slow this code down. 😆
{ | ||
// Assume "application/x-www-form-urlencoded" as the default media type | ||
// to match the default assumed in IFormFeature. | ||
supportedRequestFormats = [new ApiRequestFormat { MediaType = "application/x-www-form-urlencoded" }]; |
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.
How does one change this?
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.
For overriding whatever content-type is used, end-users can add Accepts
metadata to the given endpoint.
[(new { Id = 1, Name = "Todo" }).GetType(), "Int32StringAnonymousType"], | ||
[typeof(IFormFile), "IFormFile"], | ||
[typeof(IFormFileCollection), "IFormFileCollection"], | ||
[typeof(Results<Ok<TodoWithDueDate>, Ok<Todo>>), "TodoWithDueDateOkTodoOkResults"], |
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.
Is the name generation defined in a spec or are we making it up?
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.
If this is supposed to be human-readable, some underscores might help.
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's not defined in the spec. The only constraints that the spec puts on these keys is that they have to be case sensitive.
I opted for alphanumeric values (avoiding things like "<" or "`").
If this is supposed to be human-readable, some underscores might help.
Adding this would help although underscores as separators isn't a convention I've usually seen. Conventionally, most APIs tend to use PascalCase for type names.
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 meant between types, not within types. TodoWithDueDateOk_TodoOk_Results
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.
That would be TodoWithDueDate_Ok_Todo_Ok_Results
in this case?
Throwing out random thought: ResultsOf_OkOf_TodoWithDueDate_And_OkOf_Todo
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.
There's enough context that the printer could skip it for arity 1, but sure, might as well go all the way. I like "Of", but I find "And" pretty wordy.
return null; | ||
} | ||
|
||
private OpenApiRequestBody GetFormRequestBody(ApiDescription description, IEnumerable<ApiParameterDescription> formParameters) |
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.
Nit: Personally, I would find it cleaner to pass in the supported request formats. It feels weird to pull the parameters out of the description and then pass both those parameters and the description they came from to this helper.
And below.
return _schemas.GetOrAdd(schemaId, _ => CreateSchema()); | ||
} | ||
|
||
internal static OpenApiSchema CreateSchema() |
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.
private?
schema.Properties[parameter.Name] = _componentService.GetOrCreateSchema(parameter.Type); | ||
} | ||
|
||
foreach (var requestForm in supportedRequestFormats) |
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 might call this "requestFormat" since we're talking about forms in the same scope.
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.
Yes, I believe this was a typo on my part. 😅
[(new { Id = 1, Name = "Todo" }).GetType(), "Int32StringAnonymousType"], | ||
[typeof(IFormFile), "IFormFile"], | ||
[typeof(IFormFileCollection), "IFormFileCollection"], | ||
[typeof(Results<Ok<TodoWithDueDate>, Ok<Todo>>), "TodoWithDueDateOkTodoOkResults"], |
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.
If this is supposed to be human-readable, some underscores might help.
[typeof(IFormFile), false], | ||
[typeof(IFormFileCollection), false], | ||
[typeof(Results<Ok<TodoWithDueDate>, Ok<Todo>>), false], | ||
[typeof(TestDelegate), false] |
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.
Test a nested type?
0f78d11
to
987a385
Compare
After noodling on it some more, I decide to key the dictionary tracking the schemas by My original prototype keyed by Also, I accidentally amended a commit while rebasing changes instead of creating a new commit. Force of habit. 😅 I'm used to rebasing and amending on branches before I make them public in a PR. This unfortunately means that the history for this PR is a little wonky. 😞 |
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 think not rolling your own type stringification was the right call. 😆
Yes -- and luckily we won't have to include this in our codepath in the long-term because the runtime's implementation will support generating schema reference IDs. |
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)
Note: I'm trying Copilot-generated pull request descriptions for the first time. 🤪
This pull request adds support for generating request body objects into the OpenAPI document.
Key changes include:
Addition of form parameters handling:
src/OpenApi/src/Extensions/ApiDescriptionExtensions.cs
: Added new extension methodsTryGetFormParameters
andTryGetBodyParameter
to retrieve form and body parameters respectively from theApiDescription
.Enhancements to
OpenApiComponentService
:src/OpenApi/src/Services/OpenApiComponentService.cs
: Introduced a concurrent dictionary_schemas
to cache OpenAPI schemas for well-defined types in ASP.NET Core. Also added theGetOrCreateSchema
method to get or create a schema for a given type.Refinements to
OpenApiDocumentService
:src/OpenApi/src/Services/OpenApiDocumentService.cs
: IntegratedOpenApiComponentService
intoOpenApiDocumentService
and added methods to handle request bodies, including form request bodies and JSON request bodies. [1] [2] [3] [4]New extension methods and tests:
src/OpenApi/src/Extensions/TypeExtensions.cs
: Added new extension methodsGetSchemaReferenceId
andIsAnonymousType
forType
.src/OpenApi/test/Extensions/TypeExtensionsTests.cs
: Added corresponding tests for the new extension methods.Minor changes and fixes:
src/OpenApi/sample/Program.cs
: ImportedMicrosoft.AspNetCore.Mvc
namespace, addedAddOpenApi
service, introduced a new mapping group "forms" with several endpoints, and updated theTodo
record. [1] [2] [3] [4]src/OpenApi/test/Services/OpenApiDocumentServiceTests.Info.cs
: ReplacedIServiceProvider
withIKeyedServiceProvider
in tests. [1] [2]