From dd6fabb42ccde1047fa9ed306ec2815317e59331 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 19 Oct 2022 15:50:56 -0700 Subject: [PATCH 1/9] Adding initial Form-support --- .../src/RequestDelegateFactory.cs | 113 +++- .../src/RequestDelegateFactoryContext.cs | 1 + .../test/RequestDelegateFactoryTests.cs | 483 ++++++++++++++++-- 3 files changed, 536 insertions(+), 61 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index b44c17c6e57f..ff20429c1d30 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" }; /// @@ -710,13 +712,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 BindParameterFromFormFile(parameter, formAttribute.Name ?? parameter.Name, factoryContext, RequestDelegateFactoryConstants.FormFileAttribute); + } + return BindParameterFromFormCollection(parameter, factoryContext); + } + + return BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext); } else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) { @@ -753,6 +764,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); @@ -1820,26 +1835,81 @@ private static void AddInferredAcceptsMetadata(RequestDelegateFactoryContext fac factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes)); } - private static Expression BindParameterFromFormFiles( + private static void TrackFormParameter( + ParameterInfo parameter, + string key, + string trackedParameterSource, + RequestDelegateFactoryContext factoryContext, + bool isFormFile = false) + { + factoryContext.FirstFormRequestBodyParameter ??= parameter; + factoryContext.TrackedParameters.Add(key, trackedParameterSource); + factoryContext.ReadForm = true; + + if (isFormFile) + { + factoryContext.ReadFormFile = true; + } + } + + private static Expression BindParameterFromFormCollection( ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) { - if (factoryContext.FirstFormRequestBodyParameter is null) + // Do not duplicate the metadata if there are multiple form parameters + if (!factoryContext.ReadForm) { - factoryContext.FirstFormRequestBodyParameter = parameter; + AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormContentType); } - factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter); + TrackFormParameter(parameter, parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter, factoryContext); + + return BindParameterFromExpression( + parameter, + FormExpr, + factoryContext, + "form"); + } + + private static Expression BindParameterFromFormItem( + ParameterInfo parameter, + string key, + RequestDelegateFactoryContext factoryContext) + { + var valueExpression = GetValueFromProperty(FormExpr, FormIndexerProperty, key, GetExpressionType(parameter.ParameterType)); // Do not duplicate the metadata if there are multiple form parameters if (!factoryContext.ReadForm) + { + AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormContentType); + } + + TrackFormParameter(parameter, key, RequestDelegateFactoryConstants.FormAttribute, factoryContext); + + return BindParameterFromValue( + parameter, + valueExpression, + factoryContext, + "form"); + } + + private static Expression BindParameterFromFormFiles( + ParameterInfo parameter, + RequestDelegateFactoryContext factoryContext) + { + // Do not duplicate the metadata if there are multiple form file parameters + if (!factoryContext.ReadFormFile) { AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); } - factoryContext.ReadForm = true; + TrackFormParameter(parameter, parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter, factoryContext, isFormFile: true); - return BindParameterFromExpression(parameter, FormFilesExpr, factoryContext, "body"); + return BindParameterFromExpression( + parameter, + FormFilesExpr, + factoryContext, + "body"); } private static Expression BindParameterFromFormFile( @@ -1848,24 +1918,21 @@ private static Expression BindParameterFromFormFile( RequestDelegateFactoryContext factoryContext, string trackedParameterSource) { - if (factoryContext.FirstFormRequestBodyParameter is null) - { - factoryContext.FirstFormRequestBodyParameter = parameter; - } - - factoryContext.TrackedParameters.Add(key, trackedParameterSource); + var valueExpression = GetValueFromProperty(FormFilesExpr, FormFilesIndexerProperty, key, typeof(IFormFile)); - // Do not duplicate the metadata if there are multiple form parameters - if (!factoryContext.ReadForm) + // Do not duplicate the metadata if there are multiple form file parameters + if (!factoryContext.ReadFormFile) { AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); } - factoryContext.ReadForm = true; - - var valueExpression = GetValueFromProperty(FormFilesExpr, FormFilesIndexerProperty, key, typeof(IFormFile)); + TrackFormParameter(parameter, key, trackedParameterSource, factoryContext, isFormFile: true); - return BindParameterFromExpression(parameter, valueExpression, factoryContext, "form file"); + return BindParameterFromExpression( + parameter, + valueExpression, + factoryContext, + "form file"); } private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, RequestDelegateFactoryContext factoryContext) @@ -2210,12 +2277,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)"; 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 65b9d7b0f964..1f0c0963ae19 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 ContextArgAccess { get; } = new(); diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 199b03fb12e5..2d12835460b0 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -920,12 +920,19 @@ public async Task RequestDelegateHandlesArraysFromExplicitQueryStringSource() httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); + httpContext.Request.Form = new FormCollection(new Dictionary + { + ["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 async Task RequestDelegateHandlesArraysFromExplicitQueryStringSource() 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 async Task RequestDelegateHandlesStringValuesFromExplicitQueryStringSourc httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); + httpContext.Request.Form = new FormCollection(new Dictionary + { + ["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 async Task RequestDelegateHandlesStringValuesFromExplicitQueryStringSourc 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 async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStr httpContext.Request.Headers["Custom"] = new(new[] { "4", "5", "6" }); + httpContext.Request.Form = new FormCollection(new Dictionary + { + ["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 async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStr 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 async Task RequestDelegateLogsStringValuesFromExplicitQueryStringSourceFo 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 async Task RequestDelegateLogsStringValuesFromExplicitQueryStringSourceFo 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 async Task RequestDelegateLogsStringValuesFromExplicitQueryStringSourceFo 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 async Task RequestDelegateHandlesNullableStringValuesFromExplicitQueryStr Assert.Null(httpContext.Items["query"]); Assert.Null(httpContext.Items["headers"]); + Assert.Null(httpContext.Items["form"]); } [Fact] @@ -4300,25 +4335,33 @@ void TestAction(IFormFile file) } [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(() => RequestDelegateFactory.Create(TestFormFileAndJson)); Assert.Throws(() => RequestDelegateFactory.Create(TestFormFilesAndJson)); Assert.Throws(() => RequestDelegateFactory.Create(TestFormFileAndJsonWithAttribute)); Assert.Throws(() => RequestDelegateFactory.Create(TestFormFileCollectionAndJson)); + Assert.Throws(() => RequestDelegateFactory.Create(TestFormCollectionAndJson)); + Assert.Throws(() => RequestDelegateFactory.Create(TestFormWithAttributeAndJson)); Assert.Throws(() => RequestDelegateFactory.Create(TestJsonAndFormFile)); Assert.Throws(() => RequestDelegateFactory.Create(TestJsonAndFormFiles)); Assert.Throws(() => RequestDelegateFactory.Create(TestJsonAndFormFileCollection)); Assert.Throws(() => RequestDelegateFactory.Create(TestJsonAndFormFileWithAttribute)); + Assert.Throws(() => RequestDelegateFactory.Create(TestJsonAndFormCollection)); + Assert.Throws(() => RequestDelegateFactory.Create(TestJsonAndFormWithAttribute)); } [Fact] @@ -4413,34 +4456,6 @@ void TestAction([FromForm(Name = "foo")] IFormFileCollection formFiles) Assert.Equal("Assigning a value to the IFromFormMetadata.Name property is not supported for parameters of type IFormFileCollection.", nse.Message); } - [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(() => RequestDelegateFactory.Create(handler)); - Assert.Equal("IFromFormMetadata is only supported for parameters of type IFormFileCollection and IFormFile.", nse.Message); - } - } - [Fact] public async Task RequestDelegatePopulatesFromIFormFileParameter() { @@ -4891,6 +4906,396 @@ void TestAction(IFormFile? file, TraceIdentifier traceId) Assert.Equal("my-trace-id", traceIdArgument.Id); } + public static TheoryData FormContent + { + get + { + var dataset = new TheoryData(); + + 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 { ["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(() => 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(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(); + 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(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(); + 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(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(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(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(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(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); + } + + [Fact] + public async Task RequestDelegatePopulatesFromBothIFormCollectionAndIFormFileParameters() + { + IFormFileCollection? formFilesArgument = null; + IFormCollection? formArgument = null; + + void TestAction(IFormCollection form, IFormFileCollection formFiles) + { + formFilesArgument = formFiles; + formArgument = form; + } + + 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(new RequestBodyDetectionFeature(true)); + + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); + + Assert.Equal(httpContext.Request.Form.Files, formFilesArgument); + Assert.NotNull(formFilesArgument!["file"]); + + 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(); + Assert.Collection(allAcceptsMetadata, + (m) => Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, m.ContentTypes), + (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(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 + { + ["tryParsable"] = "42" + }); + + var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromForm]int tryParsable) => + { + httpContext.Items["tryParsable"] = tryParsable; + }); + + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); + + Assert.Equal(42, httpContext.Items["tryParsable"]); + } + private record struct ParameterListRecordStruct(HttpContext HttpContext, [FromRoute] int Value); private record ParameterListRecordClass(HttpContext HttpContext, [FromRoute] int Value); @@ -4946,7 +5351,7 @@ public ParameterListStructWithMultipleParameterizedContructor(HttpContext httpCo 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 +5609,7 @@ void TestAction([AsParameters] ParameterListRecordWitDefaultValue args) 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 +6350,13 @@ public static object[][] TasksOfTypesMethods { ValueTask ValueTaskOfStructMethod() { - return ValueTask.FromResult(new TodoStruct { Name = "Test todo"}); + return ValueTask.FromResult(new TodoStruct { Name = "Test todo" }); } async ValueTask ValueTaskOfStructWithYieldMethod() { await Task.Yield(); - return new TodoStruct { Name = "Test todo" }; + return new TodoStruct { Name = "Test todo" }; } Task TaskOfStructMethod() From 833ce3854f7623bf3ba9c6a8fecff1f01d8726b6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 19 Oct 2022 16:04:12 -0700 Subject: [PATCH 2/9] clean up --- .../src/RequestDelegateFactory.cs | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index ff20429c1d30..0b07218b73bc 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1835,23 +1835,6 @@ private static void AddInferredAcceptsMetadata(RequestDelegateFactoryContext fac factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes)); } - private static void TrackFormParameter( - ParameterInfo parameter, - string key, - string trackedParameterSource, - RequestDelegateFactoryContext factoryContext, - bool isFormFile = false) - { - factoryContext.FirstFormRequestBodyParameter ??= parameter; - factoryContext.TrackedParameters.Add(key, trackedParameterSource); - factoryContext.ReadForm = true; - - if (isFormFile) - { - factoryContext.ReadFormFile = true; - } - } - private static Expression BindParameterFromFormCollection( ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) @@ -1862,13 +1845,15 @@ private static Expression BindParameterFromFormCollection( AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormContentType); } - TrackFormParameter(parameter, parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter, factoryContext); + factoryContext.FirstFormRequestBodyParameter ??= parameter; + factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter); + factoryContext.ReadForm = true; return BindParameterFromExpression( parameter, FormExpr, factoryContext, - "form"); + "body"); } private static Expression BindParameterFromFormItem( @@ -1884,7 +1869,9 @@ private static Expression BindParameterFromFormItem( AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormContentType); } - TrackFormParameter(parameter, key, RequestDelegateFactoryConstants.FormAttribute, factoryContext); + factoryContext.FirstFormRequestBodyParameter ??= parameter; + factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute); + factoryContext.ReadForm = true; return BindParameterFromValue( parameter, @@ -1903,7 +1890,10 @@ private static Expression BindParameterFromFormFiles( AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); } - TrackFormParameter(parameter, parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter, factoryContext, isFormFile: true); + factoryContext.FirstFormRequestBodyParameter ??= parameter; + factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter); + factoryContext.ReadForm = true; + factoryContext.ReadFormFile = true; return BindParameterFromExpression( parameter, @@ -1926,7 +1916,10 @@ private static Expression BindParameterFromFormFile( AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); } - TrackFormParameter(parameter, key, trackedParameterSource, factoryContext, isFormFile: true); + factoryContext.FirstFormRequestBodyParameter ??= parameter; + factoryContext.TrackedParameters.Add(key, trackedParameterSource); + factoryContext.ReadForm = true; + factoryContext.ReadFormFile = true; return BindParameterFromExpression( parameter, From c931b8ec2992788656e3613f514c5c2a0e527ff1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 31 Oct 2022 16:43:26 -0700 Subject: [PATCH 3/9] PR feedback --- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 2 +- src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 0b07218b73bc..c0aa0269fc71 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1846,7 +1846,7 @@ private static Expression BindParameterFromFormCollection( } factoryContext.FirstFormRequestBodyParameter ??= parameter; - factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter); + factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormCollectionParameter); factoryContext.ReadForm = true; return BindParameterFromExpression( diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 2d12835460b0..5129817efe7e 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -5228,6 +5228,7 @@ void TestAction(IFormCollection form, IFormFileCollection formFiles) Assert.Equal(httpContext.Request.Form.Files, formFilesArgument); Assert.NotNull(formFilesArgument!["file"]); + Assert.Equal("file.txt", formFilesArgument!["file"]!.Name); Assert.Equal(httpContext.Request.Form, formArgument); Assert.NotNull(formArgument); From 65745d3dcac09885f944ddbb6b0dff634f1a69f2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 31 Oct 2022 16:48:06 -0700 Subject: [PATCH 4/9] PR feedback --- .../Http.Extensions/test/RequestDelegateFactoryTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 5129817efe7e..25b9e1043cdc 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -5282,10 +5282,10 @@ public async Task RequestDelegatePopulatesTryParsableParametersFromForm() httpContext.Request.Form = new FormCollection(new Dictionary { - ["tryParsable"] = "42" + ["tryParsable"] = "https://example.org" }); - var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromForm]int tryParsable) => + var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromForm] MyTryParseRecord tryParsable) => { httpContext.Items["tryParsable"] = tryParsable; }); @@ -5294,7 +5294,8 @@ public async Task RequestDelegatePopulatesTryParsableParametersFromForm() await requestDelegate(httpContext); - Assert.Equal(42, httpContext.Items["tryParsable"]); + var content = Assert.IsType(httpContext.Items["tryParsable"]); + Assert.Equal(new Uri("https://example.org"), content.Uri); } private record struct ParameterListRecordStruct(HttpContext HttpContext, [FromRoute] int Value); From dadca5284b51a9237b26bc08d6a763e01beb4f35 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 1 Nov 2022 10:40:36 -0700 Subject: [PATCH 5/9] Fix unit test --- src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 25b9e1043cdc..0a7595c14414 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -5228,7 +5228,7 @@ void TestAction(IFormCollection form, IFormFileCollection formFiles) Assert.Equal(httpContext.Request.Form.Files, formFilesArgument); Assert.NotNull(formFilesArgument!["file"]); - Assert.Equal("file.txt", formFilesArgument!["file"]!.Name); + Assert.Equal("file.txt", formFilesArgument!["file"]!.FileName); Assert.Equal(httpContext.Request.Form, formArgument); Assert.NotNull(formArgument); From 5c892fe4ee017556ed15070f782a7209467bb228 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 1 Nov 2022 13:34:54 -0700 Subject: [PATCH 6/9] Adding Form Accepts Metadata later --- .../src/RequestDelegateFactory.cs | 44 +++++++++---------- .../test/RequestDelegateFactoryTests.cs | 38 +++++++++++----- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index c0aa0269fc71..71c18fbc7440 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -8,6 +9,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Reflection.Metadata; using System.Runtime.CompilerServices; using System.Security.Claims; using System.Text; @@ -379,6 +381,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 @@ -1835,16 +1843,22 @@ private static void AddInferredAcceptsMetadata(RequestDelegateFactoryContext fac factoryContext.EndpointBuilder.Metadata.Add(new AcceptsMetadata(type, factoryContext.AllowEmptyRequestBody, contentTypes)); } - private static Expression BindParameterFromFormCollection( - ParameterInfo parameter, - RequestDelegateFactoryContext factoryContext) + private static void InferFormAcceptsMetadata(RequestDelegateFactoryContext factoryContext) { - // Do not duplicate the metadata if there are multiple form parameters - if (!factoryContext.ReadForm) + if (factoryContext.ReadFormFile) { - AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormContentType); + AddInferredAcceptsMetadata(factoryContext, factoryContext.FirstFormRequestBodyParameter!.ParameterType, FormFileContentType); } + else + { + 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; @@ -1863,12 +1877,6 @@ private static Expression BindParameterFromFormItem( { var valueExpression = GetValueFromProperty(FormExpr, FormIndexerProperty, key, GetExpressionType(parameter.ParameterType)); - // Do not duplicate the metadata if there are multiple form parameters - if (!factoryContext.ReadForm) - { - AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormContentType); - } - factoryContext.FirstFormRequestBodyParameter ??= parameter; factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute); factoryContext.ReadForm = true; @@ -1884,12 +1892,6 @@ private static Expression BindParameterFromFormFiles( ParameterInfo parameter, RequestDelegateFactoryContext factoryContext) { - // Do not duplicate the metadata if there are multiple form file parameters - if (!factoryContext.ReadFormFile) - { - AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); - } - factoryContext.FirstFormRequestBodyParameter ??= parameter; factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.FormFileParameter); factoryContext.ReadForm = true; @@ -1910,12 +1912,6 @@ private static Expression BindParameterFromFormFile( { var valueExpression = GetValueFromProperty(FormFilesExpr, FormFilesIndexerProperty, key, typeof(IFormFile)); - // Do not duplicate the metadata if there are multiple form file parameters - if (!factoryContext.ReadFormFile) - { - AddInferredAcceptsMetadata(factoryContext, parameter.ParameterType, FormFileContentType); - } - factoryContext.FirstFormRequestBodyParameter ??= parameter; factoryContext.TrackedParameters.Add(key, trackedParameterSource); factoryContext.ReadForm = true; diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 0a7595c14414..db4cae0aef6d 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -5194,18 +5194,34 @@ void TestAction([FromForm] string? message, TraceIdentifier traceId) Assert.Equal("my-trace-id", traceIdArgument.Id); } - [Fact] - public async Task RequestDelegatePopulatesFromBothIFormCollectionAndIFormFileParameters() + public static IEnumerable FormAndFormFileParametersDelegates { - IFormFileCollection? formFilesArgument = null; - IFormCollection? formArgument = null; - - void TestAction(IFormCollection form, IFormFileCollection formFiles) + get { - formFilesArgument = formFiles; - formArgument = form; + 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 + { + new object?[] { TestAction }, + new object?[] { 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"); @@ -5221,11 +5237,14 @@ void TestAction(IFormCollection form, IFormFileCollection formFiles) httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary"; httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var factoryResult = RequestDelegateFactory.Create(TestAction); + 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); @@ -5241,7 +5260,6 @@ void TestAction(IFormCollection form, IFormFileCollection formFiles) var allAcceptsMetadata = factoryResult.EndpointMetadata.OfType(); Assert.Collection(allAcceptsMetadata, - (m) => Assert.Equal(new[] { "multipart/form-data", "application/x-www-form-urlencoded" }, m.ContentTypes), (m) => Assert.Equal(new[] { "multipart/form-data" }, m.ContentTypes)); } From 455735ee18431af20915ae01bc5e4e2962b260a6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 1 Nov 2022 13:36:37 -0700 Subject: [PATCH 7/9] clean up --- src/Http/Http.Extensions/src/RequestDelegateFactory.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 71c18fbc7440..cc9f5dadfb0c 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -9,7 +8,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Reflection.Metadata; using System.Runtime.CompilerServices; using System.Security.Claims; using System.Text; From a8c18b3b5ab1585bf8566289a40ec3531763bec0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 1 Nov 2022 14:50:03 -0700 Subject: [PATCH 8/9] Fix warnings --- .../Http.Extensions/test/RequestDelegateFactoryTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index db4cae0aef6d..91cd99c4a7c8 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -5194,7 +5194,7 @@ void TestAction([FromForm] string? message, TraceIdentifier traceId) Assert.Equal("my-trace-id", traceIdArgument.Id); } - public static IEnumerable FormAndFormFileParametersDelegates + public static IEnumerable FormAndFormFileParametersDelegates { get { @@ -5210,10 +5210,10 @@ void TestActionDifferentOrder(HttpContext context, IFormFileCollection formFiles context.Items["FormArgument"] = form; } - return new List + return new List { - new object?[] { TestAction }, - new object?[] { TestActionDifferentOrder }, + new object[] { (Action)TestAction }, + new object[] { (Action)TestActionDifferentOrder }, }; } } From 1b4a884b5c7a73badf00179b3c13612b187f0839 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 3 Nov 2022 13:03:41 -0700 Subject: [PATCH 9/9] Adding a test for InferMetadata --- .../test/RequestDelegateFactoryTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 91cd99c4a7c8..d4a3ee1e4231 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -6886,6 +6886,27 @@ public void InferMetadata_ThenCreate_CombinesAllMetadata_InCorrectOrder() m => Assert.True(m is CustomEndpointMetadata { Source: MetadataSource.Caller })); } + [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(); + 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() {