diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs index 0aafd008c5d1..399c61755280 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs @@ -18,9 +18,12 @@ internal static void EmitQueryOrHeaderParameterPreparation(this EndpointParamete { codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment()); - var assigningCode = endpointParameter.Source is EndpointParameterSource.Header - ? $"httpContext.Request.Headers[\"{endpointParameter.LookupName}\"]" - : $"httpContext.Request.Query[\"{endpointParameter.LookupName}\"]"; + var assigningCode = (endpointParameter.Source, endpointParameter.IsArray) switch + { + (EndpointParameterSource.Header, true) => $"httpContext.Request.Headers.GetCommaSeparatedValues(\"{endpointParameter.LookupName}\")", + (EndpointParameterSource.Header, false) => $"httpContext.Request.Headers[\"{endpointParameter.LookupName}\"]", + _ => $"httpContext.Request.Query[\"{endpointParameter.LookupName}\"]" + }; codeWriter.WriteLine($"var {endpointParameter.EmitAssigningCodeResult()} = {assigningCode};"); // If we are not optional, then at this point we can just assign the string value to the handler argument, diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 25f2513c7d4e..c0eaa1c56032 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -52,6 +52,7 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo ExecuteTaskResultOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ExecuteAwaitedReturnMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteAwaitedReturn), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo GetHeaderSplitMethod = typeof(ParsingHelpers).GetMethod(nameof(ParsingHelpers.GetHeaderSplit), BindingFlags.Public | BindingFlags.Static, [typeof(IHeaderDictionary), typeof(string)])!; private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; private static readonly MethodInfo GetServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; private static readonly MethodInfo GetRequiredKeyedServiceMethod = typeof(ServiceProviderKeyedServiceExtensions).GetMethod(nameof(ServiceProviderKeyedServiceExtensions.GetRequiredKeyedService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider), typeof(object) })!; @@ -1955,8 +1956,14 @@ private static Expression BindParameterFromExpression( Expression.Convert(Expression.Constant(parameter.DefaultValue), parameter.ParameterType))); } - private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, PropertyInfo itemProperty, string key, RequestDelegateFactoryContext factoryContext, string source) => - BindParameterFromValue(parameter, GetValueFromProperty(property, itemProperty, key, GetExpressionType(parameter.ParameterType)), factoryContext, source); + private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, PropertyInfo itemProperty, string key, RequestDelegateFactoryContext factoryContext, string source) + { + var valueExpression = (source == "header" && parameter.ParameterType.IsArray) + ? Expression.Call(GetHeaderSplitMethod, property, Expression.Constant(key)) + : GetValueFromProperty(property, itemProperty, key, GetExpressionType(parameter.ParameterType)); + + return BindParameterFromValue(parameter, valueExpression, factoryContext, source); + } private static Type? GetExpressionType(Type type) => type.IsArray ? typeof(string[]) : diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index 2afff2e67bd7..083da9bd7aed 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -3156,6 +3156,55 @@ static void TestAction([AsParameters] ParameterListRequiredNullableStringFromDif Assert.Null(httpContext.Items["RequiredHeaderParam"]); } + private class ParameterListFromHeaderCommaSeparatedValues + { + [FromHeader(Name = "q")] + public required StringValues? BoundToStringValues { get; set; } + + [FromHeader(Name = "q")] + public string? BoundToString { get; set; } + + [FromHeader(Name = "q")] + public string[]? BoundToStringArray { get; set; } + + [FromHeader(Name = "q")] + public int[]? BoundToIntArray { get; set; } + } + + [Theory] + [InlineData("", new string[] { }, new int[] {})] + [InlineData(" ", new string[] { }, new int[] { })] + [InlineData(",", new string[] { }, new int[] { })] + [InlineData("100", new string[] { "100" }, new int[] { 100 })] + [InlineData("1,2", new string[] { "1", "2" }, new int[] { 1, 2 })] + [InlineData("1, 2 , 3", new string[] { "1", "2", "3" }, new int[] { 1, 2, 3 })] + public async Task RequestDelegateFactory_FromHeader_CommaSeparatedValues(string headerValue, string[] expectedStringArray, int[] expectedIntArray) + { + // Arrange + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["q"] = headerValue; + + void TestAction([AsParameters] ParameterListFromHeaderCommaSeparatedValues args) + { + httpContext.Items[nameof(args.BoundToStringValues)] = args.BoundToStringValues; + httpContext.Items[nameof(args.BoundToString)] = args.BoundToString; + httpContext.Items[nameof(args.BoundToStringArray)] = args.BoundToStringArray; + httpContext.Items[nameof(args.BoundToIntArray)] = args.BoundToIntArray; + } + + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; + + // Act + await requestDelegate(httpContext); + + // Assert + Assert.Equal(headerValue, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToString)]); + Assert.Equal(new StringValues(headerValue), httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToStringValues)]); + Assert.Equal(expectedStringArray, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToStringArray)]); + Assert.Equal(expectedIntArray, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToIntArray)]); + } + #nullable disable private class ParameterListMixedRequiredStringsFromDifferentSources { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt index 3002127aa54c..3a8e7fe8928b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Http.Generated { var wasParamCheckFailure = false; // Endpoint Parameter: p (Type = Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[], IsOptional = False, IsParsable = True, IsArray = True, Source = Header) - var p_raw = httpContext.Request.Headers["p"]; + var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p"); var p_temp = p_raw.ToArray(); global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[] p_local = new global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[p_temp.Length]; for (var i = 0; i < p_temp.Length; i++) @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Http.Generated { var wasParamCheckFailure = false; // Endpoint Parameter: p (Type = Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[], IsOptional = False, IsParsable = True, IsArray = True, Source = Header) - var p_raw = httpContext.Request.Headers["p"]; + var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p"); var p_temp = p_raw.ToArray(); global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[] p_local = new global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[p_temp.Length]; for (var i = 0; i < p_temp.Length; i++) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt index c4463d8d32ac..6365d1485f43 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Http.Generated { var wasParamCheckFailure = false; // Endpoint Parameter: p (Type = string?[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header) - var p_raw = httpContext.Request.Headers["p"]; + var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p"); var p_temp = p_raw.ToArray(); string[] p_local = p_temp!; @@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Http.Generated { var wasParamCheckFailure = false; // Endpoint Parameter: p (Type = string?[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header) - var p_raw = httpContext.Request.Headers["p"]; + var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p"); var p_temp = p_raw.ToArray(); string[] p_local = p_temp!; diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt index 040fdfca9d93..66c4504e000b 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Http.Generated { var wasParamCheckFailure = false; // Endpoint Parameter: p (Type = string[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header) - var p_raw = httpContext.Request.Headers["p"]; + var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p"); var p_temp = p_raw.ToArray(); string[] p_local = p_temp!; @@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Http.Generated { var wasParamCheckFailure = false; // Endpoint Parameter: p (Type = string[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header) - var p_raw = httpContext.Request.Headers["p"]; + var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p"); var p_temp = p_raw.ToArray(); string[] p_local = p_temp!; diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs index 275cffd93822..e460f9e6ee84 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs @@ -775,4 +775,23 @@ public async Task RequestDelegateHandlesArraysFromExplicitQueryStringSource() Assert.Equal(new[] { 4, 5, 6 }, (int[])httpContext.Items["headers"]!); Assert.Equal(new[] { 7, 8, 9 }, (int[])httpContext.Items["form"]!); } + + [Theory] + [InlineData("""app.MapGet("/", ([FromHeader(Name = "q")] string[]? arr) => arr);""", "", "[]")] + [InlineData("""app.MapGet("/", ([FromHeader(Name = "q")] string[]? arr) => arr);""", "a,b,c", "[\"a\",\"b\",\"c\"]")] + [InlineData("""app.MapGet("/", ([FromHeader(Name = "q")] int[]? arr) => arr);""", "1,2,3", "[1,2,3]")] + public async Task MapMethods_Get_With_CommaSeparatedValues_InHeader_ShouldBindToArray(string source, string headerContent, string expectedBody) + { + var (_, compilation) = await RunGeneratorAsync(source); + var endpoints = GetEndpointsFromCompilation(compilation); + + foreach (var endpoint in endpoints) + { + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["q"] = headerContent; + await endpoint.RequestDelegate(httpContext); + + await VerifyResponseBodyAsync(httpContext, expectedBody); + } + } }