Skip to content

Commit 99c501c

Browse files
authored
Fix for: Minimal APIs and controllers treat header arrays differently (#58251)
* #54978 * update EndpointParameterEmitter * update snapshots * go back 4 commits in googletest * imporove BindParameterFromProperty(...) readability * get coverage for both compile-time and run-time code gen with a single test case. * merging main * Revert "go back 4 commits in googletest" This reverts commit ba2d730. * Revert "merging main" This reverts commit b618f3c. * empty * add caching for GetHeaderSplit MethodInfo
1 parent e512a11 commit 99c501c

File tree

7 files changed

+89
-11
lines changed

7 files changed

+89
-11
lines changed

src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator/StaticRouteHandlerModel/Emitters/EndpointParameterEmitter.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ internal static void EmitQueryOrHeaderParameterPreparation(this EndpointParamete
1818
{
1919
codeWriter.WriteLine(endpointParameter.EmitParameterDiagnosticComment());
2020

21-
var assigningCode = endpointParameter.Source is EndpointParameterSource.Header
22-
? $"httpContext.Request.Headers[\"{endpointParameter.LookupName}\"]"
23-
: $"httpContext.Request.Query[\"{endpointParameter.LookupName}\"]";
21+
var assigningCode = (endpointParameter.Source, endpointParameter.IsArray) switch
22+
{
23+
(EndpointParameterSource.Header, true) => $"httpContext.Request.Headers.GetCommaSeparatedValues(\"{endpointParameter.LookupName}\")",
24+
(EndpointParameterSource.Header, false) => $"httpContext.Request.Headers[\"{endpointParameter.LookupName}\"]",
25+
_ => $"httpContext.Request.Query[\"{endpointParameter.LookupName}\"]"
26+
};
2427
codeWriter.WriteLine($"var {endpointParameter.EmitAssigningCodeResult()} = {assigningCode};");
2528

2629
// If we are not optional, then at this point we can just assign the string value to the handler argument,

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public static partial class RequestDelegateFactory
5252
private static readonly MethodInfo ExecuteTaskResultOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
5353
private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!;
5454
private static readonly MethodInfo ExecuteAwaitedReturnMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteAwaitedReturn), BindingFlags.NonPublic | BindingFlags.Static)!;
55+
private static readonly MethodInfo GetHeaderSplitMethod = typeof(ParsingHelpers).GetMethod(nameof(ParsingHelpers.GetHeaderSplit), BindingFlags.Public | BindingFlags.Static, [typeof(IHeaderDictionary), typeof(string)])!;
5556
private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
5657
private static readonly MethodInfo GetServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!;
5758
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(
19551956
Expression.Convert(Expression.Constant(parameter.DefaultValue), parameter.ParameterType)));
19561957
}
19571958

1958-
private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, PropertyInfo itemProperty, string key, RequestDelegateFactoryContext factoryContext, string source) =>
1959-
BindParameterFromValue(parameter, GetValueFromProperty(property, itemProperty, key, GetExpressionType(parameter.ParameterType)), factoryContext, source);
1959+
private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, PropertyInfo itemProperty, string key, RequestDelegateFactoryContext factoryContext, string source)
1960+
{
1961+
var valueExpression = (source == "header" && parameter.ParameterType.IsArray)
1962+
? Expression.Call(GetHeaderSplitMethod, property, Expression.Constant(key))
1963+
: GetValueFromProperty(property, itemProperty, key, GetExpressionType(parameter.ParameterType));
1964+
1965+
return BindParameterFromValue(parameter, valueExpression, factoryContext, source);
1966+
}
19601967

19611968
private static Type? GetExpressionType(Type type) =>
19621969
type.IsArray ? typeof(string[]) :

src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3156,6 +3156,55 @@ static void TestAction([AsParameters] ParameterListRequiredNullableStringFromDif
31563156
Assert.Null(httpContext.Items["RequiredHeaderParam"]);
31573157
}
31583158

3159+
private class ParameterListFromHeaderCommaSeparatedValues
3160+
{
3161+
[FromHeader(Name = "q")]
3162+
public required StringValues? BoundToStringValues { get; set; }
3163+
3164+
[FromHeader(Name = "q")]
3165+
public string? BoundToString { get; set; }
3166+
3167+
[FromHeader(Name = "q")]
3168+
public string[]? BoundToStringArray { get; set; }
3169+
3170+
[FromHeader(Name = "q")]
3171+
public int[]? BoundToIntArray { get; set; }
3172+
}
3173+
3174+
[Theory]
3175+
[InlineData("", new string[] { }, new int[] {})]
3176+
[InlineData(" ", new string[] { }, new int[] { })]
3177+
[InlineData(",", new string[] { }, new int[] { })]
3178+
[InlineData("100", new string[] { "100" }, new int[] { 100 })]
3179+
[InlineData("1,2", new string[] { "1", "2" }, new int[] { 1, 2 })]
3180+
[InlineData("1, 2 , 3", new string[] { "1", "2", "3" }, new int[] { 1, 2, 3 })]
3181+
public async Task RequestDelegateFactory_FromHeader_CommaSeparatedValues(string headerValue, string[] expectedStringArray, int[] expectedIntArray)
3182+
{
3183+
// Arrange
3184+
var httpContext = CreateHttpContext();
3185+
httpContext.Request.Headers["q"] = headerValue;
3186+
3187+
void TestAction([AsParameters] ParameterListFromHeaderCommaSeparatedValues args)
3188+
{
3189+
httpContext.Items[nameof(args.BoundToStringValues)] = args.BoundToStringValues;
3190+
httpContext.Items[nameof(args.BoundToString)] = args.BoundToString;
3191+
httpContext.Items[nameof(args.BoundToStringArray)] = args.BoundToStringArray;
3192+
httpContext.Items[nameof(args.BoundToIntArray)] = args.BoundToIntArray;
3193+
}
3194+
3195+
var factoryResult = RequestDelegateFactory.Create(TestAction);
3196+
var requestDelegate = factoryResult.RequestDelegate;
3197+
3198+
// Act
3199+
await requestDelegate(httpContext);
3200+
3201+
// Assert
3202+
Assert.Equal(headerValue, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToString)]);
3203+
Assert.Equal(new StringValues(headerValue), httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToStringValues)]);
3204+
Assert.Equal(expectedStringArray, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToStringArray)]);
3205+
Assert.Equal(expectedIntArray, httpContext.Items[nameof(ParameterListFromHeaderCommaSeparatedValues.BoundToIntArray)]);
3206+
}
3207+
31593208
#nullable disable
31603209
private class ParameterListMixedRequiredStringsFromDifferentSources
31613210
{

src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_ComplexTypeArrayParam.generated.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Http.Generated
108108
{
109109
var wasParamCheckFailure = false;
110110
// Endpoint Parameter: p (Type = Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[], IsOptional = False, IsParsable = True, IsArray = True, Source = Header)
111-
var p_raw = httpContext.Request.Headers["p"];
111+
var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p");
112112
var p_temp = p_raw.ToArray();
113113
global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[] p_local = new global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[p_temp.Length];
114114
for (var i = 0; i < p_temp.Length; i++)
@@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Http.Generated
138138
{
139139
var wasParamCheckFailure = false;
140140
// Endpoint Parameter: p (Type = Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[], IsOptional = False, IsParsable = True, IsArray = True, Source = Header)
141-
var p_raw = httpContext.Request.Headers["p"];
141+
var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p");
142142
var p_temp = p_raw.ToArray();
143143
global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[] p_local = new global::Microsoft.AspNetCore.Http.Generators.Tests.ParsableTodo[p_temp.Length];
144144
for (var i = 0; i < p_temp.Length; i++)

src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_NullableStringArrayParam.generated.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Http.Generated
108108
{
109109
var wasParamCheckFailure = false;
110110
// Endpoint Parameter: p (Type = string?[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header)
111-
var p_raw = httpContext.Request.Headers["p"];
111+
var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p");
112112
var p_temp = p_raw.ToArray();
113113
string[] p_local = p_temp!;
114114

@@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Http.Generated
125125
{
126126
var wasParamCheckFailure = false;
127127
// Endpoint Parameter: p (Type = string?[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header)
128-
var p_raw = httpContext.Request.Headers["p"];
128+
var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p");
129129
var p_temp = p_raw.ToArray();
130130
string[] p_local = p_temp!;
131131

src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapAction_ExplicitHeader_StringArrayParam.generated.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.Http.Generated
108108
{
109109
var wasParamCheckFailure = false;
110110
// Endpoint Parameter: p (Type = string[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header)
111-
var p_raw = httpContext.Request.Headers["p"];
111+
var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p");
112112
var p_temp = p_raw.ToArray();
113113
string[] p_local = p_temp!;
114114

@@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.Http.Generated
125125
{
126126
var wasParamCheckFailure = false;
127127
// Endpoint Parameter: p (Type = string[], IsOptional = False, IsParsable = False, IsArray = True, Source = Header)
128-
var p_raw = httpContext.Request.Headers["p"];
128+
var p_raw = httpContext.Request.Headers.GetCommaSeparatedValues("p");
129129
var p_temp = p_raw.ToArray();
130130
string[] p_local = p_temp!;
131131

src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.Arrays.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,4 +775,23 @@ public async Task RequestDelegateHandlesArraysFromExplicitQueryStringSource()
775775
Assert.Equal(new[] { 4, 5, 6 }, (int[])httpContext.Items["headers"]!);
776776
Assert.Equal(new[] { 7, 8, 9 }, (int[])httpContext.Items["form"]!);
777777
}
778+
779+
[Theory]
780+
[InlineData("""app.MapGet("/", ([FromHeader(Name = "q")] string[]? arr) => arr);""", "", "[]")]
781+
[InlineData("""app.MapGet("/", ([FromHeader(Name = "q")] string[]? arr) => arr);""", "a,b,c", "[\"a\",\"b\",\"c\"]")]
782+
[InlineData("""app.MapGet("/", ([FromHeader(Name = "q")] int[]? arr) => arr);""", "1,2,3", "[1,2,3]")]
783+
public async Task MapMethods_Get_With_CommaSeparatedValues_InHeader_ShouldBindToArray(string source, string headerContent, string expectedBody)
784+
{
785+
var (_, compilation) = await RunGeneratorAsync(source);
786+
var endpoints = GetEndpointsFromCompilation(compilation);
787+
788+
foreach (var endpoint in endpoints)
789+
{
790+
var httpContext = CreateHttpContext();
791+
httpContext.Request.Headers["q"] = headerContent;
792+
await endpoint.RequestDelegate(httpContext);
793+
794+
await VerifyResponseBodyAsync(httpContext, expectedBody);
795+
}
796+
}
778797
}

0 commit comments

Comments
 (0)