Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruno Oliveira <brunolins16@users.noreply.github.com>2022-11-09 01:50:11 +0300
committerGitHub <noreply@github.com>2022-11-09 01:50:11 +0300
commitac50803c5a8f7a04621df30846b6f97634966f31 (patch)
treee3e05c2125ab72fbcad5c893fabf9f047f876070
parent1bee0afeedab9d6d9d1cf23e65daa7ea5fcc6d47 (diff)
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
-rw-r--r--src/Http/Http.Extensions/src/RequestDelegateFactory.cs116
-rw-r--r--src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs1
-rw-r--r--src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs524
3 files changed, 572 insertions, 69 deletions
diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
index b44c17c6e5..cc9f5dadfb 100644
--- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
+++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs
@@ -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)!;
@@ -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>
@@ -377,6 +379,12 @@ public static partial class RequestDelegateFactory
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
@@ -710,13 +718,22 @@ public static partial class RequestDelegateFactory
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)))
{
@@ -753,6 +770,10 @@ public static partial class RequestDelegateFactory
{
return RequestAbortedExpr;
}
+ else if (parameter.ParameterType == typeof(IFormCollection))
+ {
+ return BindParameterFromFormCollection(parameter, factoryContext);
+ }
else if (parameter.ParameterType == typeof(IFormFileCollection))
{
return BindParameterFromFormFiles(parameter, factoryContext);
@@ -1820,52 +1841,85 @@ public static partial class RequestDelegateFactory
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;
+
+ 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");
}
private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, RequestDelegateFactoryContext factoryContext)
@@ -2210,12 +2264,14 @@ public static partial class RequestDelegateFactory
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)";
public const string PropertyAsParameter = "As Parameter (Attribute)";
}
diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs
index 65b9d7b0f9..1f0c0963ae 100644
--- a/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs
+++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryContext.cs
@@ -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();
diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
index 199b03fb12..d4a3ee1e42 100644
--- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
+++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
@@ -920,12 +920,19 @@ public partial class RequestDelegateFactoryTests : LoggedTest
httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" });
+ httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
+ {
+ ["form"] = new(new[] { "7", "8", "9" })
+ });
+
var factoryResult = RequestDelegateFactory.Create((HttpContext context,
[FromHeader(Name = "Custom")] int[] headerValues,
- [FromQuery(Name = "a")] int[] queryValues) =>
+ [FromQuery(Name = "a")] int[] queryValues,
+ [FromForm(Name = "form")] int[] formValues) =>
{
context.Items["headers"] = headerValues;
context.Items["query"] = queryValues;
+ context.Items["form"] = formValues;
});
var requestDelegate = factoryResult.RequestDelegate;
@@ -934,6 +941,7 @@ public partial class RequestDelegateFactoryTests : LoggedTest
Assert.Equal(new[] { 1, 2, 3 }, (int[])httpContext.Items["query"]!);
Assert.Equal(new[] { 4, 5, 6 }, (int[])httpContext.Items["headers"]!);
+ Assert.Equal(new[] { 7, 8, 9 }, (int[])httpContext.Items["form"]!);
}
[Fact]
@@ -947,12 +955,19 @@ public partial class RequestDelegateFactoryTests : LoggedTest
httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" });
+ httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
+ {
+ ["form"] = new(new[] { "7", "8", "9" })
+ });
+
var factoryResult = RequestDelegateFactory.Create((HttpContext context,
[FromHeader(Name = "Custom")] StringValues headerValues,
- [FromQuery(Name = "a")] StringValues queryValues) =>
+ [FromQuery(Name = "a")] StringValues queryValues,
+ [FromForm(Name = "form")] StringValues formValues) =>
{
context.Items["headers"] = headerValues;
context.Items["query"] = queryValues;
+ context.Items["form"] = formValues;
});
var requestDelegate = factoryResult.RequestDelegate;
@@ -961,6 +976,7 @@ public partial class RequestDelegateFactoryTests : LoggedTest
Assert.Equal(new StringValues(new[] { "1", "2", "3" }), httpContext.Items["query"]);
Assert.Equal(new StringValues(new[] { "4", "5", "6" }), httpContext.Items["headers"]);
+ Assert.Equal(new StringValues(new[] { "7", "8", "9" }), httpContext.Items["form"]!);
}
[Fact]
@@ -974,12 +990,19 @@ public partial class RequestDelegateFactoryTests : LoggedTest
httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" });
+ httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
+ {
+ ["form"] = new(new[] { "7", "8", "9" })
+ });
+
var factoryResult = RequestDelegateFactory.Create((HttpContext context,
[FromHeader(Name = "Custom")] StringValues? headerValues,
- [FromQuery(Name = "a")] StringValues? queryValues) =>
+ [FromQuery(Name = "a")] StringValues? queryValues,
+ [FromForm(Name = "form")] StringValues? formValues) =>
{
context.Items["headers"] = headerValues;
context.Items["query"] = queryValues;
+ context.Items["form"] = formValues;
});
var requestDelegate = factoryResult.RequestDelegate;
@@ -988,6 +1011,7 @@ public partial class RequestDelegateFactoryTests : LoggedTest
Assert.Equal(new StringValues(new[] { "1", "2", "3" }), httpContext.Items["query"]);
Assert.Equal(new StringValues(new[] { "4", "5", "6" }), httpContext.Items["headers"]);
+ Assert.Equal(new StringValues(new[] { "7", "8", "9" }), httpContext.Items["form"]!);
}
[Fact]
@@ -996,10 +1020,12 @@ public partial class RequestDelegateFactoryTests : LoggedTest
var invoked = false;
var httpContext = CreateHttpContext();
+ httpContext.Request.Form = new FormCollection(null);
var factoryResult = RequestDelegateFactory.Create((HttpContext context,
[FromHeader(Name = "foo")] StringValues headerValues,
- [FromQuery(Name = "bar")] StringValues queryValues) =>
+ [FromQuery(Name = "bar")] StringValues queryValues,
+ [FromForm(Name = "form")] StringValues formValues) =>
{
invoked = true;
});
@@ -1015,7 +1041,7 @@ public partial class RequestDelegateFactoryTests : LoggedTest
var logs = TestSink.Writes.ToArray();
- Assert.Equal(2, logs.Length);
+ Assert.Equal(3, logs.Length);
Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
@@ -1024,21 +1050,29 @@ public partial class RequestDelegateFactoryTests : LoggedTest
Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
Assert.Equal(@"Required parameter ""StringValues queryValues"" was not provided from query string.", logs[1].Message);
+
+ Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[2].EventId);
+ Assert.Equal(LogLevel.Debug, logs[2].LogLevel);
+ Assert.Equal(@"Required parameter ""StringValues formValues"" was not provided from form.", logs[2].Message);
}
[Fact]
public async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStringSourceForUnpresentedValues()
{
var httpContext = CreateHttpContext();
+ httpContext.Request.Form = new FormCollection(null);
var factoryResult = RequestDelegateFactory.Create((HttpContext context,
[FromHeader(Name = "foo")] StringValues? headerValues,
- [FromQuery(Name = "bar")] StringValues? queryValues) =>
+ [FromQuery(Name = "bar")] StringValues? queryValues,
+ [FromForm(Name = "form")] StringValues? formValues) =>
{
Assert.False(headerValues.HasValue);
Assert.False(queryValues.HasValue);
+ Assert.False(formValues.HasValue);
context.Items["headers"] = headerValues;
context.Items["query"] = queryValues;
+ context.Items["form"] = formValues;
});
var requestDelegate = factoryResult.RequestDelegate;
@@ -1047,6 +1081,7 @@ public partial class RequestDelegateFactoryTests : LoggedTest
Assert.Null(httpContext.Items["query"]);
Assert.Null(httpContext.Items["headers"]);
+ Assert.Null(httpContext.Items["form"]);
}
[Fact]
@@ -4300,25 +4335,33 @@ public partial class RequestDelegateFactoryTests : LoggedTest
}
[Fact]
- public void BuildRequestDelegateThrowsInvalidOperationExceptionBodyAndFormFileParameters()
+ public void BuildRequestDelegateThrowsInvalidOperationExceptionBodyAndFormParameters()
{
void TestFormFileAndJson(IFormFile value1, Todo value2) { }
void TestFormFilesAndJson(IFormFile value1, IFormFile value2, Todo value3) { }
void TestFormFileCollectionAndJson(IFormFileCollection value1, Todo value2) { }
void TestFormFileAndJsonWithAttribute(IFormFile value1, [FromBody] int value2) { }
+ void TestFormCollectionAndJson(IFormCollection value1, Todo value2) { }
+ void TestFormWithAttributeAndJson([FromForm] string value1, Todo value2) { }
void TestJsonAndFormFile(Todo value1, IFormFile value2) { }
void TestJsonAndFormFiles(Todo value1, IFormFile value2, IFormFile value3) { }
void TestJsonAndFormFileCollection(Todo value1, IFormFileCollection value2) { }
void TestJsonAndFormFileWithAttribute(Todo value1, [FromForm] IFormFile value2) { }
+ void TestJsonAndFormCollection(Todo value1, IFormCollection value2) { }
+ void TestJsonAndFormWithAttribute(Todo value1, [FromForm] string value2) { }
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFileAndJson));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFilesAndJson));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFileAndJsonWithAttribute));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormFileCollectionAndJson));
+ Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormCollectionAndJson));
+ Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestFormWithAttributeAndJson));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFile));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFiles));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFileCollection));
Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormFileWithAttribute));
+ Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormCollection));
+ Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(TestJsonAndFormWithAttribute));
}
[Fact]
@@ -4414,34 +4457,6 @@ public partial class RequestDelegateFactoryTests : LoggedTest
}
[Fact]
- public void CreateThrowsNotSupportedExceptionIfFromFormParameterIsNotIFormFileCollectionOrIFormFile()
- {
- void TestActionBool([FromForm] bool value) { };
- void TestActionInt([FromForm] int value) { };
- void TestActionObject([FromForm] object value) { };
- void TestActionString([FromForm] string value) { };
- void TestActionCancellationToken([FromForm] CancellationToken value) { };
- void TestActionClaimsPrincipal([FromForm] ClaimsPrincipal value) { };
- void TestActionHttpContext([FromForm] HttpContext value) { };
- void TestActionIFormCollection([FromForm] IFormCollection value) { };
-
- AssertNotSupportedExceptionThrown(TestActionBool);
- AssertNotSupportedExceptionThrown(TestActionInt);
- AssertNotSupportedExceptionThrown(TestActionObject);
- AssertNotSupportedExceptionThrown(TestActionString);
- AssertNotSupportedExceptionThrown(TestActionCancellationToken);
- AssertNotSupportedExceptionThrown(TestActionClaimsPrincipal);
- AssertNotSupportedExceptionThrown(TestActionHttpContext);
- AssertNotSupportedExceptionThrown(TestActionIFormCollection);
-
- static void AssertNotSupportedExceptionThrown(Delegate handler)
- {
- var nse = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(handler));
- Assert.Equal("IFromFormMetadata is only supported for parameters of type IFormFileCollection and IFormFile.", nse.Message);
- }
- }
-
- [Fact]
public async Task RequestDelegatePopulatesFromIFormFileParameter()
{
IFormFile? fileArgument = null;
@@ -4891,6 +4906,416 @@ public partial class RequestDelegateFactoryTests : LoggedTest
Assert.Equal("my-trace-id", traceIdArgument.Id);
}
+ public static TheoryData<HttpContent, string> FormContent
+ {
+ get
+ {
+ var dataset = new TheoryData<HttpContent, string>();
+
+ var multipartFormData = new MultipartFormDataContent("some-boundary");
+ multipartFormData.Add(new StringContent("hello"), "message");
+ multipartFormData.Add(new StringContent("foo"), "name");
+ dataset.Add(multipartFormData, "multipart/form-data;boundary=some-boundary");
+
+ var urlEncondedForm = new FormUrlEncodedContent(new Dictionary<string, string> { ["message"] = "hello", ["name"] = "foo" });
+ dataset.Add(urlEncondedForm, "application/x-www-form-urlencoded");
+
+ return dataset;
+ }
+ }
+
+ [Fact]
+ public void CreateThrowsNotSupportedExceptionIfIFormCollectionHasMetadataParameterName()
+ {
+ IFormCollection? formArgument = null;
+
+ void TestAction([FromForm(Name = "foo")] IFormCollection formCollection)
+ {
+ formArgument = formCollection;
+ }
+
+ var nse = Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestAction));
+ Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormCollection.", nse.Message);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegatePopulatesFromIFormCollectionParameter(HttpContent content, string contentType)
+ {
+ IFormCollection? formArgument = null;
+
+ void TestAction(IFormCollection formCollection)
+ {
+ formArgument = formCollection;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.Equal(httpContext.Request.Form, formArgument);
+ Assert.NotNull(formArgument);
+ Assert.Collection(formArgument!,
+ (item) =>
+ {
+ Assert.Equal("message", item.Key);
+ Assert.Equal("hello", item.Value);
+ },
+ (item) =>
+ {
+ Assert.Equal("name", item.Key);
+ Assert.Equal("foo", item.Value);
+ });
+
+ var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
+ var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+ Assert.NotNull(acceptsMetadata);
+ Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegatePopulatesFromIFormCollectionParameterWithAttribute(HttpContent content, string contentType)
+ {
+ IFormCollection? formArgument = null;
+
+ void TestAction([FromForm] IFormCollection formCollection)
+ {
+ formArgument = formCollection;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.Equal(httpContext.Request.Form, formArgument);
+ Assert.NotNull(formArgument);
+ Assert.Collection(formArgument!,
+ (item) =>
+ {
+ Assert.Equal("message", item.Key);
+ Assert.Equal("hello", item.Value);
+ },
+ (item) =>
+ {
+ Assert.Equal("name", item.Key);
+ Assert.Equal("foo", item.Value);
+ });
+
+ var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
+ var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+ Assert.NotNull(acceptsMetadata);
+ Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegatePopulatesFromOptionalFormParameter(HttpContent content, string contentType)
+ {
+ string? messageArgument = null;
+
+ void TestAction([FromForm] string? message)
+ {
+ messageArgument = message;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.Equal(httpContext.Request.Form["message"], messageArgument);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegatePopulatesFromMultipleRequiredFormParameters(HttpContent content, string contentType)
+ {
+ string? messageArgument = null;
+ string? nameArgument = null;
+
+ void TestAction([FromForm] string message, [FromForm] string name)
+ {
+ messageArgument = message;
+ nameArgument = name;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.Equal(httpContext.Request.Form["message"], messageArgument);
+ Assert.NotNull(messageArgument);
+
+ Assert.Equal(httpContext.Request.Form["name"], nameArgument);
+ Assert.NotNull(nameArgument);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegatePopulatesFromOptionalMissingFormParameter(HttpContent content, string contentType)
+ {
+ string? messageArgument = null;
+ string? additionalMessageArgument = null;
+
+ void TestAction([FromForm] string? message, [FromForm] string? additionalMessage)
+ {
+ messageArgument = message;
+ additionalMessageArgument = additionalMessage;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.Equal(httpContext.Request.Form["message"], messageArgument);
+ Assert.NotNull(messageArgument);
+ Assert.Null(additionalMessageArgument);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegatePopulatesFromFormParameterWithMetadata(HttpContent content, string contentType)
+ {
+ string? textArgument = null;
+
+ void TestAction([FromForm(Name = "message")] string text)
+ {
+ textArgument = text;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.Equal(httpContext.Request.Form["message"], textArgument);
+ Assert.NotNull(textArgument);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegatePopulatesFromFormAndBoundParameter(HttpContent content, string contentType)
+ {
+ string? messageArgument = null;
+ TraceIdentifier traceIdArgument = default;
+
+ void TestAction([FromForm] string? message, TraceIdentifier traceId)
+ {
+ messageArgument = message;
+ traceIdArgument = traceId;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+ httpContext.TraceIdentifier = "my-trace-id";
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.Equal(httpContext.Request.Form["message"], messageArgument);
+ Assert.NotNull(messageArgument);
+
+ Assert.Equal("my-trace-id", traceIdArgument.Id);
+ }
+
+ public static IEnumerable<object[]> FormAndFormFileParametersDelegates
+ {
+ get
+ {
+ void TestAction(HttpContext context, IFormCollection form, IFormFileCollection formFiles)
+ {
+ context.Items["FormFilesArgument"] = formFiles;
+ context.Items["FormArgument"] = form;
+ }
+
+ void TestActionDifferentOrder(HttpContext context, IFormFileCollection formFiles, IFormCollection form)
+ {
+ context.Items["FormFilesArgument"] = formFiles;
+ context.Items["FormArgument"] = form;
+ }
+
+ return new List<object[]>
+ {
+ new object[] { (Action<HttpContext, IFormCollection, IFormFileCollection>)TestAction },
+ new object[] { (Action<HttpContext, IFormFileCollection, IFormCollection>)TestActionDifferentOrder },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(FormAndFormFileParametersDelegates))]
+ public async Task RequestDelegatePopulatesFromBothIFormCollectionAndIFormFileParameters(Delegate action)
+ {
+ var fileContent = new StringContent("hello", Encoding.UTF8, "application/octet-stream");
+ var form = new MultipartFormDataContent("some-boundary");
+ form.Add(fileContent, "file", "file.txt");
+ form.Add(new StringContent("foo"), "name");
+
+ var stream = new MemoryStream();
+ await form.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(action);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ IFormFileCollection? formFilesArgument = httpContext.Items["FormFilesArgument"] as IFormFileCollection;
+ IFormCollection? formArgument = httpContext.Items["FormArgument"] as IFormCollection;
+
+ Assert.Equal(httpContext.Request.Form.Files, formFilesArgument);
+ Assert.NotNull(formFilesArgument!["file"]);
+ Assert.Equal("file.txt", formFilesArgument!["file"]!.FileName);
+
+ Assert.Equal(httpContext.Request.Form, formArgument);
+ Assert.NotNull(formArgument);
+ Assert.Collection(formArgument!,
+ (item) =>
+ {
+ Assert.Equal("name", item.Key);
+ Assert.Equal("foo", item.Value);
+ });
+
+ var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType<IAcceptsMetadata>();
+ Assert.Collection(allAcceptsMetadata,
+ (m) => Assert.Equal(new[] { "multipart/form-data" }, m.ContentTypes));
+ }
+
+ [Theory]
+ [MemberData(nameof(FormContent))]
+ public async Task RequestDelegateSets400ResponseIfRequiredFormItemNotSpecified(HttpContent content, string contentType)
+ {
+ var invoked = false;
+
+ void TestAction([FromForm] string unknownParameter)
+ {
+ invoked = true;
+ }
+
+ var stream = new MemoryStream();
+ await content.CopyToAsync(stream);
+
+ stream.Seek(0, SeekOrigin.Begin);
+
+ var httpContext = CreateHttpContext();
+ httpContext.Request.Body = stream;
+ httpContext.Request.Headers["Content-Type"] = contentType;
+ httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+ var factoryResult = RequestDelegateFactory.Create(TestAction);
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ Assert.False(invoked);
+ Assert.Equal(400, httpContext.Response.StatusCode);
+ }
+
+ [Fact]
+ public async Task RequestDelegatePopulatesTryParsableParametersFromForm()
+ {
+ var httpContext = CreateHttpContext();
+
+ httpContext.Request.Form = new FormCollection(new Dictionary<string, StringValues>
+ {
+ ["tryParsable"] = "https://example.org"
+ });
+
+ var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromForm] MyTryParseRecord tryParsable) =>
+ {
+ httpContext.Items["tryParsable"] = tryParsable;
+ });
+
+ var requestDelegate = factoryResult.RequestDelegate;
+
+ await requestDelegate(httpContext);
+
+ var content = Assert.IsType<MyTryParseRecord>(httpContext.Items["tryParsable"]);
+ Assert.Equal(new Uri("https://example.org"), content.Uri);
+ }
+
private record struct ParameterListRecordStruct(HttpContext HttpContext, [FromRoute] int Value);
private record ParameterListRecordClass(HttpContext HttpContext, [FromRoute] int Value);
@@ -4946,7 +5371,7 @@ public partial class RequestDelegateFactoryTests : LoggedTest
Value = 10;
}
- public ParameterListStructWithMultipleParameterizedContructor(HttpContext httpContext, [FromHeader(Name ="Value")] int value)
+ public ParameterListStructWithMultipleParameterizedContructor(HttpContext httpContext, [FromHeader(Name = "Value")] int value)
{
HttpContext = httpContext;
Value = value;
@@ -5204,7 +5629,7 @@ public partial class RequestDelegateFactoryTests : LoggedTest
private class ParameterListWitDefaultValue
{
- public ParameterListWitDefaultValue(HttpContext httpContext, [FromRoute]int value = 42)
+ public ParameterListWitDefaultValue(HttpContext httpContext, [FromRoute] int value = 42)
{
HttpContext = httpContext;
Value = value;
@@ -5945,13 +6370,13 @@ public partial class RequestDelegateFactoryTests : LoggedTest
{
ValueTask<TodoStruct> ValueTaskOfStructMethod()
{
- return ValueTask.FromResult(new TodoStruct { Name = "Test todo"});
+ return ValueTask.FromResult(new TodoStruct { Name = "Test todo" });
}
async ValueTask<TodoStruct> ValueTaskOfStructWithYieldMethod()
{
await Task.Yield();
- return new TodoStruct { Name = "Test todo" };
+ return new TodoStruct { Name = "Test todo" };
}
Task<TodoStruct> TaskOfStructMethod()
@@ -6462,6 +6887,27 @@ public partial class RequestDelegateFactoryTests : LoggedTest
}
[Fact]
+ public void InferMetadata_PopulatesAcceptsMetadata_WhenReadFromForm()
+ {
+ // Arrange
+ var @delegate = void (IFormCollection formCollection) => { };
+ var options = new RequestDelegateFactoryOptions
+ {
+ EndpointBuilder = CreateEndpointBuilder(),
+ };
+
+ // Act
+ var metadataResult = RequestDelegateFactory.InferMetadata(@delegate.Method, options);
+
+ // Assert
+ var allAcceptsMetadata = metadataResult.EndpointMetadata.OfType<IAcceptsMetadata>();
+ var acceptsMetadata = Assert.Single(allAcceptsMetadata);
+
+ Assert.NotNull(acceptsMetadata);
+ Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, acceptsMetadata.ContentTypes);
+ }
+
+ [Fact]
public void Create_AllowsRemovalOfDefaultMetadata_ByReturnTypesImplementingIEndpointMetadataProvider()
{
// Arrange