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

Fix form handling and add NuGet pkg docs #55321

Merged
merged 4 commits into from Apr 29, 2024
Merged

Conversation

captainsafia
Copy link
Member

Address #53831, #46746, and #52284

I'm working through the backlog of bug reports on OpenAPI and adding test cases for each one. While doing this, I encountered issues with the way we are currently handling:

  • APIs that consume multiple arguments from the form body
  • Controller-based APIs that consume arguments from the form body

This PR adds test coverage for these cases and updates the logic for generating form schemas to support both cases.

@captainsafia captainsafia requested a review from a team as a code owner April 23, 2024 20:33
@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Apr 23, 2024
@captainsafia captainsafia added area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Apr 23, 2024
Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

Early questions

@@ -42,7 +42,26 @@ private readonly ConcurrentDictionary<(Type, ParameterInfo?), JsonObject> _schem
{
OnSchemaGenerated = (context, schema) =>
{
schema.ApplyPrimitiveTypesAndFormats(context.TypeInfo.Type);
var type = context.TypeInfo.Type;
// Fix up schemas generated for IFormFile, IFormFileCollection, Stream, and PipeReader
Copy link
Member

Choose a reason for hiding this comment

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

Add any tests with Stream and PipeReader?

Copy link
Member Author

Choose a reason for hiding this comment

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

Add tests here. It appears that Stream/PipeReader get the same treatment as FormFiles when procued by the ApiExplorer layer for MVC. Added a note about this in #55349.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

More questions.

var schema = new OpenApiSchema { Type = "object", Properties = new Dictionary<string, OpenApiSchema>() };
foreach (var parameter in formParameters)
// Group form parameters by their parameter name because MVC explodes form parameters that are bound from the
// same model instance into separate parameters, while minimal APIs does not.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// same model instance into separate parameters, while minimal APIs does not.
// same model instance into separate parameters in ApiExplorer, while minimal APIs does not.

schema.Properties[parameter.Name] = _componentService.GetOrCreateSchema(parameter.Type);
if (parameter.All(parameter => parameter.ModelMetadata.ContainerType is null))
{
var description = parameter.Single();
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why it follows that if the container types are all null there must only be one item in the group.

Copy link
Member Author

Choose a reason for hiding this comment

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

parameter.ModelMetadata.ContainerType is null when the form arguments haven't been exploded into seperate ApiParameterDescriptions as in the MVC model. I'll add a comment about this.

{
if (hasMultipleFormParameters)
{
if (description.ModelMetadata.IsComplexType)
Copy link
Member

Choose a reason for hiding this comment

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

This is pulled out because we can refer to the complex type by name and don't have to create an inline schema? Assuming so, is it possible for the complex type to be anonymous or unnameable?

Copy link
Member Author

Choose a reason for hiding this comment

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

All the schemas that we generate are currently inline at the moment. This is more-so about the fact that schemas for complex types don't need to be nested under the parameter name based on the way parameters are bound from the form.

For an endpoint like:

app.MapPost("/", ([FromForm] Todo todo => { });

We want to generate the following schema:

{
	"properties": {
		"id":  { ... },
		"title": { ... },
		"isCompleted": { ... }
	}
}

Not this one:

{
	"properties": {
		"todo": {
			"properties": {
				"id":  { ... },
				"title": { ... },
				"isCompleted": { ... }
			}
		}
	}
}

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

More questions.

schema.ApplyPrimitiveTypesAndFormats(context.TypeInfo.Type);
var type = context.TypeInfo.Type;
// Fix up schemas generated for IFormFile, IFormFileCollection, Stream, and PipeReader
// that appear as properties within complex types.
Copy link
Member

Choose a reason for hiding this comment

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

I think the goal here is to take a type like class Todo { public Stream Stream { get; set; } } and update the schema for the Stream property. Assuming so, does/should this act recursively?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ish. The underlying schema generator will use $ref to process the recursive type so the schema generated at the top-level will work for this. We don't handle ref IDs properly at the moment (follow-up work incoming) so the schemas are only partially correct. I'll add a test to reflect the current state of things.

var schema = new OpenApiSchema { Type = "object", Properties = new Dictionary<string, OpenApiSchema>() };
foreach (var parameter in formParameters)
// Group form parameters by their parameter name because MVC explodes form parameters that are bound from the
Copy link
Member

Choose a reason for hiding this comment

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

I think this sentence is distinguishing between "form parameter" and "parameter", but I'm not sure how. I think the latter might correspond to MVC action parameters?

Copy link
Member Author

Choose a reason for hiding this comment

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

This grouping will apply to all form parameters, regardless of whether we are currently processing them for an MVC action or minimal endpoint. The comment is intended to clarify why grouping by name allows us to "un-explode" the ApiParameterDescriptions that are produced by ApiExplorer specifically for MVC.

// public void PostMvc([FromForm] Todo person) { }
// app.MapGet("/form-todo", ([FromForm] Todo todo) => Results.Ok(todo));
//
// In the example above, MVC will bind four separate arguments to the Todo model while minimal APIs will
Copy link
Member

Choose a reason for hiding this comment

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

I think, in this context, "bind" refers to how things are exposed in the API explorer, rather than to what we call "parameter binding" 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.

Yep, this is about ApiExplorer. I'll tweak the verbiage.

Type = "object",
Properties = new Dictionary<string, OpenApiSchema>
{
[description.Name] = _componentService.GetOrCreateSchema(description.Type)
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it would be more readable with _componentService.GetOrCreateSchema(description.Type) pulled out. That might make it clearer that the same value is just being slotted into different places in the schema.

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 idea!

@amcasey
Copy link
Member

amcasey commented Apr 26, 2024

This and this could affect correctness. The rest are basically hygiene suggestions or questions for my own edification.

@captainsafia captainsafia merged commit 6d9dca9 into main Apr 29, 2024
26 checks passed
@captainsafia captainsafia deleted the safia/openapi-tests-docs branch April 29, 2024 22:11
@dotnet-policy-service dotnet-policy-service bot added this to the 9.0-preview5 milestone Apr 29, 2024
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.

None yet

3 participants