Skip to content

Commit ac50803

Browse files
authored
Initial Form-binding support (#44653)
* Adding initial Form-support * clean up * PR feedback * PR feedback * Fix unit test * Adding Form Accepts Metadata later * clean up * Fix warnings * Adding a test for InferMetadata
1 parent 1bee0af commit ac50803

File tree

3 files changed

+572
-69
lines changed

3 files changed

+572
-69
lines changed

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

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public static partial class RequestDelegateFactory
6161
private static readonly PropertyInfo RouteValuesIndexerProperty = typeof(RouteValueDictionary).GetProperty("Item")!;
6262
private static readonly PropertyInfo HeaderIndexerProperty = typeof(IHeaderDictionary).GetProperty("Item")!;
6363
private static readonly PropertyInfo FormFilesIndexerProperty = typeof(IFormFileCollection).GetProperty("Item")!;
64+
private static readonly PropertyInfo FormIndexerProperty = typeof(IFormCollection).GetProperty("Item")!;
6465

6566
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(WriteJsonResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
6667

@@ -110,6 +111,7 @@ public static partial class RequestDelegateFactory
110111

111112
private static readonly string[] DefaultAcceptsAndProducesContentType = new[] { JsonConstants.JsonContentType };
112113
private static readonly string[] FormFileContentType = new[] { "multipart/form-data" };
114+
private static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" };
113115
private static readonly string[] PlaintextContentType = new[] { "text/plain" };
114116

115117
/// <summary>
@@ -377,6 +379,12 @@ private static Expression[] CreateArgumentsAndInferMetadata(MethodInfo methodInf
377379

378380
if (!factoryContext.MetadataAlreadyInferred)
379381
{
382+
if (factoryContext.ReadForm)
383+
{
384+
// Add the Accepts metadata when reading from FORM.
385+
InferFormAcceptsMetadata(factoryContext);
386+
}
387+
380388
PopulateBuiltInResponseTypeMetadata(methodInfo.ReturnType, factoryContext.EndpointBuilder);
381389

382390
// Add metadata provided by the delegate return type and parameter types next, this will be more specific than inferred metadata from above
@@ -710,13 +718,22 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
710718

711719
return BindParameterFromFormFiles(parameter, factoryContext);
712720
}
713-
else if (parameter.ParameterType != typeof(IFormFile))
721+
else if (parameter.ParameterType == typeof(IFormFile))
714722
{
715-
throw new NotSupportedException(
716-
$"{nameof(IFromFormMetadata)} is only supported for parameters of type {nameof(IFormFileCollection)} and {nameof(IFormFile)}.");
723+
return BindParameterFromFormFile(parameter, formAttribute.Name ?? parameter.Name, factoryContext, RequestDelegateFactoryConstants.FormFileAttribute);
724+
}
725+
else if (parameter.ParameterType == typeof(IFormCollection))
726+
{
727+
if (!string.IsNullOrEmpty(formAttribute.Name))
728+
{
729+
throw new NotSupportedException(
730+
$"Assigning a value to the {nameof(IFromFormMetadata)}.{nameof(IFromFormMetadata.Name)} property is not supported for parameters of type {nameof(IFormCollection)}.");
731+
732+
}
733+
return BindParameterFromFormCollection(parameter, factoryContext);
717734
}
718735

719-
return BindParameterFromFormFile(parameter, formAttribute.Name ?? parameter.Name, factoryContext, RequestDelegateFactoryConstants.FormFileAttribute);
736+
return BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext);
720737
}
721738
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
722739
{
@@ -753,6 +770,10 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
753770
{
754771
return RequestAbortedExpr;
755772
}
773+
else if (parameter.ParameterType == typeof(IFormCollection))
774+
{
775+
return BindParameterFromFormCollection(parameter, factoryContext);
776+
}
756777
else if (parameter.ParameterType == typeof(IFormFileCollection))
757778
{
758779
return BindParameterFromFormFiles(parameter, factoryContext);
@@ -1820,52 +1841,85 @@ private static void AddInferredAcceptsMetadata(RequestDelegateFactoryContext fac
18201841
factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes));
18211842
}
18221843

1823-
private static Expression BindParameterFromFormFiles(
1824-
ParameterInfo parameter,
1825-
RequestDelegateFactoryContext factoryContext)
1844+
private static void InferFormAcceptsMetadata(RequestDelegateFactoryContext factoryContext)
18261845
{
1827-
if (factoryContext.FirstFormRequestBodyParameter is null)
1846+
if (factoryContext.ReadFormFile)
18281847
{
1829-
factoryContext.FirstFormRequestBodyParameter = parameter;
1848+
AddInferredAcceptsMetadata(factoryContext, factoryContext.FirstFormRequestBodyParameter!.ParameterType, FormFileContentType);
18301849
}
1831-
1832-
factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter);
1833-
1834-
// Do not duplicate the metadata if there are multiple form parameters
1835-
if (!factoryContext.ReadForm)
1850+
else
18361851
{
1837-
AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType);
1852+
AddInferredAcceptsMetadata(factoryContext, factoryContext.FirstFormRequestBodyParameter!.ParameterType, FormContentType);
18381853
}
1854+
}
18391855

1856+
private static Expression BindParameterFromFormCollection(
1857+
ParameterInfo parameter,
1858+
RequestDelegateFactoryContext factoryContext)
1859+
{
1860+
factoryContext.FirstFormRequestBodyParameter ??= parameter;
1861+
factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormCollectionParameter);
18401862
factoryContext.ReadForm = true;
18411863

1842-
return BindParameterFromExpression(parameter, FormFilesExpr, factoryContext, "body");
1864+
return BindParameterFromExpression(
1865+
parameter,
1866+
FormExpr,
1867+
factoryContext,
1868+
"body");
18431869
}
18441870

1845-
private static Expression BindParameterFromFormFile(
1871+
private static Expression BindParameterFromFormItem(
18461872
ParameterInfo parameter,
18471873
string key,
1848-
RequestDelegateFactoryContext factoryContext,
1849-
string trackedParameterSource)
1874+
RequestDelegateFactoryContext factoryContext)
18501875
{
1851-
if (factoryContext.FirstFormRequestBodyParameter is null)
1852-
{
1853-
factoryContext.FirstFormRequestBodyParameter = parameter;
1854-
}
1876+
var valueExpression = GetValueFromProperty(FormExpr, FormIndexerProperty, key, GetExpressionType(parameter.ParameterType));
18551877

1856-
factoryContext.TrackedParameters.Add(key, trackedParameterSource);
1878+
factoryContext.FirstFormRequestBodyParameter ??= parameter;
1879+
factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute);
1880+
factoryContext.ReadForm = true;
18571881

1858-
// Do not duplicate the metadata if there are multiple form parameters
1859-
if (!factoryContext.ReadForm)
1860-
{
1861-
AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType);
1862-
}
1882+
return BindParameterFromValue(
1883+
parameter,
1884+
valueExpression,
1885+
factoryContext,
1886+
"form");
1887+
}
18631888

1889+
private static Expression BindParameterFromFormFiles(
1890+
ParameterInfo parameter,
1891+
RequestDelegateFactoryContext factoryContext)
1892+
{
1893+
factoryContext.FirstFormRequestBodyParameter ??= parameter;
1894+
factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter);
18641895
factoryContext.ReadForm = true;
1896+
factoryContext.ReadFormFile = true;
1897+
1898+
return BindParameterFromExpression(
1899+
parameter,
1900+
FormFilesExpr,
1901+
factoryContext,
1902+
"body");
1903+
}
18651904

1905+
private static Expression BindParameterFromFormFile(
1906+
ParameterInfo parameter,
1907+
string key,
1908+
RequestDelegateFactoryContext factoryContext,
1909+
string trackedParameterSource)
1910+
{
18661911
var valueExpression = GetValueFromProperty(FormFilesExpr, FormFilesIndexerProperty, key, typeof(IFormFile));
18671912

1868-
return BindParameterFromExpression(parameter, valueExpression, factoryContext, "form file");
1913+
factoryContext.FirstFormRequestBodyParameter ??= parameter;
1914+
factoryContext.TrackedParameters.Add(key, trackedParameterSource);
1915+
factoryContext.ReadForm = true;
1916+
factoryContext.ReadFormFile = true;
1917+
1918+
return BindParameterFromExpression(
1919+
parameter,
1920+
valueExpression,
1921+
factoryContext,
1922+
"form file");
18691923
}
18701924

18711925
private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, RequestDelegateFactoryContext factoryContext)
@@ -2210,12 +2264,14 @@ private static class RequestDelegateFactoryConstants
22102264
public const string BodyAttribute = "Body (Attribute)";
22112265
public const string ServiceAttribute = "Service (Attribute)";
22122266
public const string FormFileAttribute = "Form File (Attribute)";
2267+
public const string FormAttribute = "Form (Attribute)";
22132268
public const string RouteParameter = "Route (Inferred)";
22142269
public const string QueryStringParameter = "Query String (Inferred)";
22152270
public const string ServiceParameter = "Services (Inferred)";
22162271
public const string BodyParameter = "Body (Inferred)";
22172272
public const string RouteOrQueryStringParameter = "Route or Query String (Inferred)";
22182273
public const string FormFileParameter = "Form File (Inferred)";
2274+
public const string FormCollectionParameter = "Form Collection (Inferred)";
22192275
public const string PropertyAsParameter = "As Parameter (Attribute)";
22202276
}
22212277

src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ internal sealed class RequestDelegateFactoryContext
4545
public NullabilityInfoContext NullabilityContext { get; } = new();
4646

4747
public bool ReadForm { get; set; }
48+
public bool ReadFormFile { get; set; }
4849
public ParameterInfo? FirstFormRequestBodyParameter { get; set; }
4950
// Properties for constructing and managing filters
5051
public List<Expression> ContextArgAccess { get; } = new();

0 commit comments

Comments
 (0)