Skip to content

Commit

Permalink
Fix form handling and add NuGet pkg docs (#55321)
Browse files Browse the repository at this point in the history
  • Loading branch information
captainsafia authored Apr 29, 2024
1 parent 3f68a51 commit 6d9dca9
Show file tree
Hide file tree
Showing 13 changed files with 1,206 additions and 40 deletions.
9 changes: 9 additions & 0 deletions src/OpenApi/sample/Controllers/TestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ public string GetByIdAndName(RouteParamsContainer paramsContainer)
return paramsContainer.Id + "_" + paramsContainer.Name;
}

[HttpPost]
[Route("/forms")]
public IActionResult PostForm([FromForm] MvcTodo todo)
{
return Ok(todo);
}

public class RouteParamsContainer
{
[FromRoute]
Expand All @@ -21,4 +28,6 @@ public class RouteParamsContainer
[MinLength(5)]
public string? Name { get; set; }
}

public record MvcTodo(string Title, string Description, bool IsCompleted);
}
2 changes: 2 additions & 0 deletions src/OpenApi/sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@

forms.MapPost("/form-file", (IFormFile resume) => Results.Ok(resume.FileName));
forms.MapPost("/form-files", (IFormFileCollection files) => Results.Ok(files.Count));
forms.MapPost("/form-file-multiple", (IFormFile resume, IFormFileCollection files) => Results.Ok(files.Count + resume.FileName));
forms.MapPost("/form-todo", ([FromForm] Todo todo) => Results.Ok(todo));
forms.MapPost("/forms-pocos-and-files", ([FromForm] Todo todo, IFormFile file) => Results.Ok(new { Todo = todo, File = file.FileName }));

var v1 = app.MapGroup("v1")
.WithGroupName("v1");
Expand Down
54 changes: 54 additions & 0 deletions src/OpenApi/src/PACKAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## About

Microsoft.AspNetCore.OpenApi is a NuGet package that provides built-in support for generating OpenAPI documents from minimal or controller-based APIs in ASP.NET Core.

## Key Features

* Supports viewing generated OpenAPI documents at runtime via a parameterized endpoint (`/openapi/{documentName}.json`)
* Supports generating an OpenAPI document at build-time
* Supports customizing the generated document via document transformers

## How to Use

To start using Microsoft.AspNetCore.OpenApi in your ASP.NET Core application, follow these steps:

### Installation

```sh
dotnet add package Microsoft.AspNetCore.OpenApi
```

### Configuration

In your Program.cs file, register the services provided by this package in the DI container and map the provided OpenAPI document endpoint in the application.

```C#
var builder = WebApplication.CreateBuilder();

// Registers the required services
builder.Services.AddOpenApi();

var app = builder.Build();

// Adds the /openapi/{documentName}.json endpoint to the application
app.MapOpenApi();

app.Run();
```

For more information on configuring and using Microsoft.AspNetCore.OpenApi, refer to the [official documentation](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/openapi).

## Main Types

<!-- The main types provided in this library -->

The main types provided by this library are:

* `OpenApiOptions`: Options for configuring OpenAPI document generation.
* `IDocumentTransformer`: Transformer that modifies the OpenAPI document generated by the library.

## Feedback & Contributing

<!-- How to provide feedback on this package and contribute to it -->

Microsoft.AspNetCore.OpenApi is released as open-source under the [MIT license](https://licenses.nuget.org/MIT). Bug reports and contributions are welcome at [the GitHub repository](https://github.com/dotnet/aspnetcore).
21 changes: 20 additions & 1 deletion src/OpenApi/src/Services/OpenApiComponentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,26 @@ internal sealed class OpenApiComponentService(IOptions<JsonOptions> jsonOptions)
{
OnSchemaGenerated = (context, schema) =>
{
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.
if (type == typeof(IFormFile) || type == typeof(Stream) || type == typeof(PipeReader))
{
schema.Clear();
schema[OpenApiSchemaKeywords.TypeKeyword] = "string";
schema[OpenApiSchemaKeywords.FormatKeyword] = "binary";
}
else if (type == typeof(IFormFileCollection))
{
schema.Clear();
schema[OpenApiSchemaKeywords.TypeKeyword] = "array";
schema[OpenApiSchemaKeywords.ItemsKeyword] = new JsonObject
{
[OpenApiSchemaKeywords.TypeKeyword] = "string",
[OpenApiSchemaKeywords.FormatKeyword] = "binary"
};
}
schema.ApplyPrimitiveTypesAndFormats(type);
if (context.GetCustomAttributes(typeof(ValidationAttribute)) is { } validationAttributes)
{
schema.ApplyValidationAttributes(validationAttributes);
Expand Down
98 changes: 94 additions & 4 deletions src/OpenApi/src/Services/OpenApiDocumentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ private OpenApiResponse GetResponse(ApiDescription apiDescription, int statusCod
.Select(responseFormat => responseFormat.MediaType);
foreach (var contentType in apiResponseFormatContentTypes)
{
var schema = apiResponseType.Type is {} type ? _componentService.GetOrCreateSchema(type) : new OpenApiSchema();
var schema = apiResponseType.Type is { } type ? _componentService.GetOrCreateSchema(type) : new OpenApiSchema();
response.Content[contentType] = new OpenApiMediaType { Schema = schema };
}

Expand Down Expand Up @@ -296,11 +296,101 @@ private OpenApiRequestBody GetFormRequestBody(IList<ApiRequestFormat> supportedR
Content = new Dictionary<string, OpenApiMediaType>()
};

// Forms are represented as objects with properties for each form field.
var schema = new OpenApiSchema { Type = "object", Properties = new Dictionary<string, OpenApiSchema>() };
foreach (var parameter in formParameters)
// Group form parameters by their name because MVC explodes form parameters that are bound from the
// same model instance into separate ApiParameterDescriptions in ApiExplorer, while minimal APIs does not.
//
// public record Todo(int Id, string Title, bool Completed, DateTime CreatedAt)
// public void PostMvc([FromForm] Todo person) { }
// app.MapGet("/form-todo", ([FromForm] Todo todo) => Results.Ok(todo));
//
// In the example above, MVC's ApiExplorer will bind four separate arguments to the Todo model while minimal APIs will
// bind a single Todo model instance to the todo parameter. Grouping by name allows us to handle both cases.
var groupedFormParameters = formParameters.GroupBy(parameter => parameter.ParameterDescriptor.Name);
// If there is only one real parameter derived from the form body, then set it directly in the schema.
var hasMultipleFormParameters = groupedFormParameters.Count() > 1;
foreach (var parameter in groupedFormParameters)
{
schema.Properties[parameter.Name] = _componentService.GetOrCreateSchema(parameter.Type);
// ContainerType is not null when the parameter has been exploded into separate API
// parameters by ApiExplorer as in the MVC model.
if (parameter.All(parameter => parameter.ModelMetadata.ContainerType is null))
{
var description = parameter.Single();
var parameterSchema = _componentService.GetOrCreateSchema(description.Type);
// Form files are keyed by their parameter name so we must capture the parameter name
// as a property in the schema.
if (description.Type == typeof(IFormFile) || description.Type == typeof(IFormFileCollection))
{
if (hasMultipleFormParameters)
{
schema.AllOf.Add(new OpenApiSchema
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>
{
[description.Name] = parameterSchema
}
});
}
else
{
schema.Properties[description.Name] = parameterSchema;
}
}
else
{
if (hasMultipleFormParameters)
{
// Here and below: POCOs do not need to be need under their parameter name in the grouping.
// The form-binding implementation will capture them implicitly.
if (description.ModelMetadata.IsComplexType)
{
schema.AllOf.Add(parameterSchema);
}
else
{
schema.AllOf.Add(new OpenApiSchema
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>
{
[description.Name] = parameterSchema
}
});
}
}
else
{
if (description.ModelMetadata.IsComplexType)
{
schema = parameterSchema;
}
else
{
schema.Properties[description.Name] = parameterSchema;
}
}
}
}
else
{
if (hasMultipleFormParameters)
{
var propertySchema = new OpenApiSchema { Type = "object", Properties = new Dictionary<string, OpenApiSchema>() };
foreach (var description in parameter)
{
propertySchema.Properties[description.Name] = _componentService.GetOrCreateSchema(description.Type);
}
schema.AllOf.Add(propertySchema);
}
else
{
foreach (var description in parameter)
{
schema.Properties[description.Name] = _componentService.GetOrCreateSchema(description.Type);
}
}
}
}

foreach (var requestFormat in supportedRequestFormats)
Expand Down
Loading

0 comments on commit 6d9dca9

Please sign in to comment.