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

Support binding to form and form file parameters in minimal actions #35158

Merged
merged 24 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ce0bcbe
Support binding to form parameters in minimal actions
martincostello Nov 6, 2021
8a0b598
Remove dead branch
martincostello Nov 6, 2021
f531d3a
Add test case for two IFormFile values
martincostello Nov 6, 2021
c4ac004
Fix IFormFile with bound parameters
martincostello Nov 6, 2021
ce19f22
Deduplicate JSON/form body reading
martincostello Nov 6, 2021
fc017c2
Add more invalid parameter tests
martincostello Nov 6, 2021
599e72c
Reduce arguments on TryRead*Async methods
martincostello Nov 6, 2021
8129be9
Clarify property name
martincostello Nov 6, 2021
d438330
Rename log message
martincostello Nov 6, 2021
e03aaf5
Reuse formatting code for parameters
martincostello Nov 6, 2021
45b30d2
Disallow form use with authentication
martincostello Nov 6, 2021
9a0e5e2
Rename metadata to IFromFormMetadata
martincostello Nov 9, 2021
8c1c4b8
Rename test class
martincostello Nov 9, 2021
7eb0242
Support IFromFormMetadata for IFormFileCollection
martincostello Nov 9, 2021
de1ace2
Add IFromFormMetadata
martincostello Nov 9, 2021
9a7cf87
Fix test compilation
martincostello Nov 9, 2021
29cbfae
Merge branch 'main' into Minimal-APIs-IForm-Support-34303
martincostello Nov 12, 2021
ec7b8df
Capture values on start-up
martincostello Nov 12, 2021
7a76199
Remove unused parameter
martincostello Nov 12, 2021
2ad668d
Throw if [FromForm] used with invalid types
martincostello Nov 12, 2021
d2de191
Update IFormFileCollection BindingSource
martincostello Nov 12, 2021
538a82e
Remove BindingSource.Form case
martincostello Nov 12, 2021
e7ba881
Update default body parameter handling
martincostello Nov 14, 2021
a169c00
Fix typo
martincostello Nov 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Http/Http.Abstractions/src/Metadata/IFromFormMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http.Metadata;

/// <summary>
/// Interface marking attributes that specify a parameter should be bound using a form field from the request body.
/// </summary>
public interface IFromFormMetadata
{
/// <summary>
/// The form field name.
/// </summary>
string? Name { get; }
}
2 changes: 2 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string!
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
563 changes: 443 additions & 120 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Large diffs are not rendered by default.

727 changes: 727 additions & 0 deletions src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,14 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
{
DisplayName = routeEndpoint.DisplayName,
RouteValues =
{
["controller"] = controllerName,
},
{
["controller"] = controllerName,
},
},
};

var hasBodyOrFormFileParameter = false;

foreach (var parameter in methodInfo.GetParameters())
{
var parameterDescription = CreateApiParameterDescription(parameter, routeEndpoint.RoutePattern);
Expand All @@ -108,23 +110,33 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
}

apiDescription.ParameterDescriptions.Add(parameterDescription);

hasBodyOrFormFileParameter |=
parameterDescription.Source == BindingSource.Body ||
parameterDescription.Source == BindingSource.FormFile;
}

// Get IAcceptsMetadata.
var acceptsMetadata = routeEndpoint.Metadata.GetMetadata<IAcceptsMetadata>();
if (acceptsMetadata is not null)
{
var acceptsRequestType = acceptsMetadata.RequestType;
var isOptional = acceptsMetadata.IsOptional;
var parameterDescription = new ApiParameterDescription
// Add a default body parameter if there was no explicitly defined parameter associated with
// either the body or a form and the user explicity defined some metadata describing the
// content types the endpoint consumes (such as Accepts<TRequest>(...) or [Consumes(...)]).
if (!hasBodyOrFormFileParameter)
{
Name = acceptsRequestType is not null ? acceptsRequestType.Name : typeof(void).Name,
ModelMetadata = CreateModelMetadata(acceptsRequestType ?? typeof(void)),
Source = BindingSource.Body,
Type = acceptsRequestType ?? typeof(void),
IsRequired = !isOptional,
};
apiDescription.ParameterDescriptions.Add(parameterDescription);
var acceptsRequestType = acceptsMetadata.RequestType;
var isOptional = acceptsMetadata.IsOptional;
var parameterDescription = new ApiParameterDescription
{
Name = acceptsRequestType is not null ? acceptsRequestType.Name : typeof(void).Name,
ModelMetadata = CreateModelMetadata(acceptsRequestType ?? typeof(void)),
Source = BindingSource.Body,
Type = acceptsRequestType ?? typeof(void),
IsRequired = !isOptional,
};
apiDescription.ParameterDescriptions.Add(parameterDescription);
}

var supportedRequestFormats = apiDescription.SupportedRequestFormats;

Expand All @@ -148,8 +160,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
var (source, name, allowEmpty, paramType) = GetBindingSourceAndName(parameter, pattern);

// Services are ignored because they are not request parameters.
// We ignore/skip body parameter because the value will be retrieved from the IAcceptsMetadata.
if (source == BindingSource.Services || source == BindingSource.Body)
if (source == BindingSource.Services)
{
return null;
}
Expand Down Expand Up @@ -239,6 +250,10 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param
{
return (BindingSource.Body, parameter.Name ?? string.Empty, fromBodyAttribute.AllowEmpty, parameter.ParameterType);
}
else if (attributes.OfType<IFromFormMetadata>().FirstOrDefault() is { } fromFormAttribute)
{
return (BindingSource.FormFile, fromFormAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
}
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)) ||
parameter.ParameterType == typeof(HttpContext) ||
parameter.ParameterType == typeof(HttpRequest) ||
Expand All @@ -265,6 +280,10 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param
return (BindingSource.Query, parameter.Name ?? string.Empty, false, displayType);
}
}
else if (parameter.ParameterType == typeof(IFormFile) || parameter.ParameterType == typeof(IFormFileCollection))
{
return (BindingSource.FormFile, parameter.Name ?? string.Empty, false, parameter.ParameterType);
}
else
{
return (BindingSource.Body, parameter.Name ?? string.Empty, false, parameter.ParameterType);
Expand Down
Loading