Skip to content

Commit 6080213

Browse files
Support binding to form and form file parameters in minimal actions (#35158)
1 parent e97bc0c commit 6080213

File tree

9 files changed

+1465
-159
lines changed

9 files changed

+1465
-159
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Interface marking attributes that specify a parameter should be bound using a form field from the request body.
8+
/// </summary>
9+
public interface IFromFormMetadata
10+
{
11+
/// <summary>
12+
/// The form field name.
13+
/// </summary>
14+
string? Name { get; }
15+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#nullable enable
22
*REMOVED*abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string!
3+
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
4+
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
35
abstract Microsoft.AspNetCore.Http.HttpResponse.ContentType.get -> string?
46
Microsoft.AspNetCore.Http.Metadata.ISkipStatusCodePagesMetadata

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 443 additions & 120 deletions
Large diffs are not rendered by default.

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 727 additions & 0 deletions
Large diffs are not rendered by default.

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,14 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
9292
{
9393
DisplayName = routeEndpoint.DisplayName,
9494
RouteValues =
95-
{
96-
["controller"] = controllerName,
97-
},
95+
{
96+
["controller"] = controllerName,
97+
},
9898
},
9999
};
100100

101+
var hasBodyOrFormFileParameter = false;
102+
101103
foreach (var parameter in methodInfo.GetParameters())
102104
{
103105
var parameterDescription = CreateApiParameterDescription(parameter, routeEndpoint.RoutePattern);
@@ -108,23 +110,33 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
108110
}
109111

110112
apiDescription.ParameterDescriptions.Add(parameterDescription);
113+
114+
hasBodyOrFormFileParameter |=
115+
parameterDescription.Source == BindingSource.Body ||
116+
parameterDescription.Source == BindingSource.FormFile;
111117
}
112118

113119
// Get IAcceptsMetadata.
114120
var acceptsMetadata = routeEndpoint.Metadata.GetMetadata<IAcceptsMetadata>();
115121
if (acceptsMetadata is not null)
116122
{
117-
var acceptsRequestType = acceptsMetadata.RequestType;
118-
var isOptional = acceptsMetadata.IsOptional;
119-
var parameterDescription = new ApiParameterDescription
123+
// Add a default body parameter if there was no explicitly defined parameter associated with
124+
// either the body or a form and the user explicity defined some metadata describing the
125+
// content types the endpoint consumes (such as Accepts<TRequest>(...) or [Consumes(...)]).
126+
if (!hasBodyOrFormFileParameter)
120127
{
121-
Name = acceptsRequestType is not null ? acceptsRequestType.Name : typeof(void).Name,
122-
ModelMetadata = CreateModelMetadata(acceptsRequestType ?? typeof(void)),
123-
Source = BindingSource.Body,
124-
Type = acceptsRequestType ?? typeof(void),
125-
IsRequired = !isOptional,
126-
};
127-
apiDescription.ParameterDescriptions.Add(parameterDescription);
128+
var acceptsRequestType = acceptsMetadata.RequestType;
129+
var isOptional = acceptsMetadata.IsOptional;
130+
var parameterDescription = new ApiParameterDescription
131+
{
132+
Name = acceptsRequestType is not null ? acceptsRequestType.Name : typeof(void).Name,
133+
ModelMetadata = CreateModelMetadata(acceptsRequestType ?? typeof(void)),
134+
Source = BindingSource.Body,
135+
Type = acceptsRequestType ?? typeof(void),
136+
IsRequired = !isOptional,
137+
};
138+
apiDescription.ParameterDescriptions.Add(parameterDescription);
139+
}
128140

129141
var supportedRequestFormats = apiDescription.SupportedRequestFormats;
130142

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

150162
// Services are ignored because they are not request parameters.
151-
// We ignore/skip body parameter because the value will be retrieved from the IAcceptsMetadata.
152-
if (source == BindingSource.Services || source == BindingSource.Body)
163+
if (source == BindingSource.Services)
153164
{
154165
return null;
155166
}
@@ -239,6 +250,10 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param
239250
{
240251
return (BindingSource.Body, parameter.Name ?? string.Empty, fromBodyAttribute.AllowEmpty, parameter.ParameterType);
241252
}
253+
else if (attributes.OfType<IFromFormMetadata>().FirstOrDefault() is { } fromFormAttribute)
254+
{
255+
return (BindingSource.FormFile, fromFormAttribute.Name ?? parameter.Name ?? string.Empty, false, parameter.ParameterType);
256+
}
242257
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)) ||
243258
parameter.ParameterType == typeof(HttpContext) ||
244259
parameter.ParameterType == typeof(HttpRequest) ||
@@ -265,6 +280,10 @@ private static ParameterDescriptor CreateParameterDescriptor(ParameterInfo param
265280
return (BindingSource.Query, parameter.Name ?? string.Empty, false, displayType);
266281
}
267282
}
283+
else if (parameter.ParameterType == typeof(IFormFile) || parameter.ParameterType == typeof(IFormFileCollection))
284+
{
285+
return (BindingSource.FormFile, parameter.Name ?? string.Empty, false, parameter.ParameterType);
286+
}
268287
else
269288
{
270289
return (BindingSource.Body, parameter.Name ?? string.Empty, false, parameter.ParameterType);

0 commit comments

Comments
 (0)