Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 11 additions & 2 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1955,8 +1955,17 @@ 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(
typeof(ParsingHelpers).GetMethod(nameof(ParsingHelpers.GetHeaderSplit), BindingFlags.Public | BindingFlags.Static, [typeof(IHeaderDictionary), typeof(string)])!,
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[]) :
Expand Down
49 changes: 49 additions & 0 deletions src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++)
Expand Down Expand Up @@ -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++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!;

Expand All @@ -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!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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!;

Expand All @@ -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!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Loading