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

Initial Form-binding support #44653

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
116 changes: 86 additions & 30 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Expand Up @@ -61,6 +61,7 @@ public static partial class RequestDelegateFactory
private static readonly PropertyInfo RouteValuesIndexerProperty = typeof(RouteValueDictionary).GetProperty("Item")!;
private static readonly PropertyInfo HeaderIndexerProperty = typeof(IHeaderDictionary).GetProperty("Item")!;
private static readonly PropertyInfo FormFilesIndexerProperty = typeof(IFormFileCollection).GetProperty("Item")!;
private static readonly PropertyInfo FormIndexerProperty = typeof(IFormCollection).GetProperty("Item")!;

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

Expand Down Expand Up @@ -110,6 +111,7 @@ public static partial class RequestDelegateFactory

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

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

if (!factoryContext.MetadataAlreadyInferred)
{
if (factoryContext.ReadForm)
{
// Add the Accepts metadata when reading from FORM.
InferFormAcceptsMetadata(factoryContext);
}

PopulateBuiltInResponseTypeMetadata(methodInfo.ReturnType, factoryContext.EndpointBuilder);

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

return BindParameterFromFormFiles(parameter, factoryContext);
}
else if (parameter.ParameterType != typeof(IFormFile))
else if (parameter.ParameterType == typeof(IFormFile))
{
throw new NotSupportedException(
$"{nameof(IFromFormMetadata)} is only supported for parameters of type {nameof(IFormFileCollection)} and {nameof(IFormFile)}.");
return BindParameterFromFormFile(parameter, formAttribute.Name ?? parameter.Name, factoryContext, RequestDelegateFactoryConstants.FormFileAttribute);
}
else if (parameter.ParameterType == typeof(IFormCollection))
{
if (!string.IsNullOrEmpty(formAttribute.Name))
{
throw new NotSupportedException(
$"Assigning a value to the {nameof(IFromFormMetadata)}.{nameof(IFromFormMetadata.Name)} property is not supported for parameters of type {nameof(IFormCollection)}.");

}
return BindParameterFromFormCollection(parameter, factoryContext);
}

return BindParameterFromFormFile(parameter, formAttribute.Name ?? parameter.Name, factoryContext, RequestDelegateFactoryConstants.FormFileAttribute);
return BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext);
}
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
{
Expand Down Expand Up @@ -753,6 +770,10 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
{
return RequestAbortedExpr;
}
else if (parameter.ParameterType == typeof(IFormCollection))
{
return BindParameterFromFormCollection(parameter, factoryContext);
}
else if (parameter.ParameterType == typeof(IFormFileCollection))
{
return BindParameterFromFormFiles(parameter, factoryContext);
Expand Down Expand Up @@ -1820,52 +1841,85 @@ private static void AddInferredAcceptsMetadata(RequestDelegateFactoryContext fac
factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes));
}

private static Expression BindParameterFromFormFiles(
ParameterInfo parameter,
RequestDelegateFactoryContext factoryContext)
private static void InferFormAcceptsMetadata(RequestDelegateFactoryContext factoryContext)
{
if (factoryContext.FirstFormRequestBodyParameter is null)
if (factoryContext.ReadFormFile)
{
factoryContext.FirstFormRequestBodyParameter = parameter;
AddInferredAcceptsMetadata(factoryContext, factoryContext.FirstFormRequestBodyParameter!.ParameterType, FormFileContentType);
}

factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter);

// Do not duplicate the metadata if there are multiple form parameters
if (!factoryContext.ReadForm)
else
{
AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType);
AddInferredAcceptsMetadata(factoryContext, factoryContext.FirstFormRequestBodyParameter!.ParameterType, FormContentType);
}
}

private static Expression BindParameterFromFormCollection(
ParameterInfo parameter,
RequestDelegateFactoryContext factoryContext)
{
factoryContext.FirstFormRequestBodyParameter ??= parameter;
factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormCollectionParameter);
factoryContext.ReadForm = true;

return BindParameterFromExpression(parameter, FormFilesExpr, factoryContext, "body");
return BindParameterFromExpression(
parameter,
FormExpr,
factoryContext,
"body");
}

private static Expression BindParameterFromFormFile(
private static Expression BindParameterFromFormItem(
ParameterInfo parameter,
string key,
RequestDelegateFactoryContext factoryContext,
string trackedParameterSource)
RequestDelegateFactoryContext factoryContext)
{
if (factoryContext.FirstFormRequestBodyParameter is null)
{
factoryContext.FirstFormRequestBodyParameter = parameter;
}
var valueExpression = GetValueFromProperty(FormExpr, FormIndexerProperty, key, GetExpressionType(parameter.ParameterType));

factoryContext.TrackedParameters.Add(key, trackedParameterSource);
factoryContext.FirstFormRequestBodyParameter ??= parameter;
factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute);
factoryContext.ReadForm = true;

// Do not duplicate the metadata if there are multiple form parameters
if (!factoryContext.ReadForm)
{
AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType);
}
return BindParameterFromValue(
parameter,
valueExpression,
factoryContext,
"form");
}

private static Expression BindParameterFromFormFiles(
ParameterInfo parameter,
RequestDelegateFactoryContext factoryContext)
{
factoryContext.FirstFormRequestBodyParameter ??= parameter;
factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter);
factoryContext.ReadForm = true;
factoryContext.ReadFormFile = true;
brunolins16 marked this conversation as resolved.
Show resolved Hide resolved

return BindParameterFromExpression(
parameter,
FormFilesExpr,
factoryContext,
"body");
}

private static Expression BindParameterFromFormFile(
ParameterInfo parameter,
string key,
RequestDelegateFactoryContext factoryContext,
string trackedParameterSource)
{
var valueExpression = GetValueFromProperty(FormFilesExpr, FormFilesIndexerProperty, key, typeof(IFormFile));

return BindParameterFromExpression(parameter, valueExpression, factoryContext, "form file");
factoryContext.FirstFormRequestBodyParameter ??= parameter;
factoryContext.TrackedParameters.Add(key, trackedParameterSource);
factoryContext.ReadForm = true;
factoryContext.ReadFormFile = true;

return BindParameterFromExpression(
parameter,
valueExpression,
factoryContext,
"form file");
Copy link
Member

Choose a reason for hiding this comment

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

Nit: make this a constant?

}

private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, RequestDelegateFactoryContext factoryContext)
Expand Down Expand Up @@ -2210,12 +2264,14 @@ private static class RequestDelegateFactoryConstants
public const string BodyAttribute = "Body (Attribute)";
public const string ServiceAttribute = "Service (Attribute)";
public const string FormFileAttribute = "Form File (Attribute)";
public const string FormAttribute = "Form (Attribute)";
public const string RouteParameter = "Route (Inferred)";
public const string QueryStringParameter = "Query String (Inferred)";
public const string ServiceParameter = "Services (Inferred)";
public const string BodyParameter = "Body (Inferred)";
public const string RouteOrQueryStringParameter = "Route or Query String (Inferred)";
public const string FormFileParameter = "Form File (Inferred)";
public const string FormCollectionParameter = "Form Collection (Inferred)";
brunolins16 marked this conversation as resolved.
Show resolved Hide resolved
public const string PropertyAsParameter = "As Parameter (Attribute)";
}

Expand Down
Expand Up @@ -45,6 +45,7 @@ internal sealed class RequestDelegateFactoryContext
public NullabilityInfoContext NullabilityContext { get; } = new();

public bool ReadForm { get; set; }
public bool ReadFormFile { get; set; }
public ParameterInfo? FirstFormRequestBodyParameter { get; set; }
// Properties for constructing and managing filters
public List<Expression> ContextArgAccess { get; } = new();
Expand Down