Skip to content

Commit f6efa13

Browse files
Add support for BindAsync without ParameterInfo (#36505)
1 parent d0464b8 commit f6efa13

File tree

4 files changed

+264
-25
lines changed

4 files changed

+264
-25
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -840,12 +840,12 @@ private static Expression BindParameterFromBindAsync(ParameterInfo parameter, Fa
840840
var isOptional = IsOptionalParameter(parameter, factoryContext);
841841

842842
// Get the BindAsync method for the type.
843-
var bindAsyncExpression = ParameterBindingMethodCache.FindBindAsyncMethod(parameter);
843+
var bindAsyncMethod = ParameterBindingMethodCache.FindBindAsyncMethod(parameter);
844844
// We know BindAsync exists because there's no way to opt-in without defining the method on the type.
845-
Debug.Assert(bindAsyncExpression is not null);
845+
Debug.Assert(bindAsyncMethod.Expression is not null);
846846

847847
// Compile the delegate to the BindAsync method for this parameter index
848-
var bindAsyncDelegate = Expression.Lambda<Func<HttpContext, ValueTask<object?>>>(bindAsyncExpression, HttpContextExpr).Compile();
848+
var bindAsyncDelegate = Expression.Lambda<Func<HttpContext, ValueTask<object?>>>(bindAsyncMethod.Expression, HttpContextExpr).Compile();
849849
factoryContext.ParameterBinders.Add(bindAsyncDelegate);
850850

851851
// boundValues[index]
@@ -854,6 +854,7 @@ private static Expression BindParameterFromBindAsync(ParameterInfo parameter, Fa
854854
if (!isOptional)
855855
{
856856
var typeName = TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false);
857+
var message = bindAsyncMethod.ParamCount == 2 ? $"{typeName}.BindAsync(HttpContext, ParameterInfo)" : $"{typeName}.BindAsync(HttpContext)";
857858
var checkRequiredBodyBlock = Expression.Block(
858859
Expression.IfThen(
859860
Expression.Equal(boundValueExpr, Expression.Constant(null)),
@@ -863,7 +864,7 @@ private static Expression BindParameterFromBindAsync(ParameterInfo parameter, Fa
863864
HttpContextExpr,
864865
Expression.Constant(typeName),
865866
Expression.Constant(parameter.Name),
866-
Expression.Constant($"{typeName}.BindAsync(HttpContext, ParameterInfo)"),
867+
Expression.Constant(message),
867868
Expression.Constant(factoryContext.ThrowOnBadRequest))
868869
)
869870
)

src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,13 @@ public async Task FindBindAsyncMethod_FindsCorrectMethodOnClass()
173173
var parameter = new MockParameterInfo(type, "bindAsyncRecord");
174174
var methodFound = cache.FindBindAsyncMethod(parameter);
175175

176-
Assert.NotNull(methodFound);
176+
Assert.NotNull(methodFound.Expression);
177+
Assert.Equal(2, methodFound.ParamCount);
177178

178179
var parsedValue = Expression.Variable(type, "parsedValue");
179180

180181
var parseHttpContext = Expression.Lambda<Func<HttpContext, ValueTask<object>>>(
181-
Expression.Block(new[] { parsedValue }, methodFound!),
182+
Expression.Block(new[] { parsedValue }, methodFound.Expression!),
182183
ParameterBindingMethodCache.HttpContextExpr).Compile();
183184

184185
var httpContext = new DefaultHttpContext
@@ -195,6 +196,37 @@ public async Task FindBindAsyncMethod_FindsCorrectMethodOnClass()
195196
Assert.Equal(new BindAsyncRecord(42), await parseHttpContext(httpContext));
196197
}
197198

199+
[Fact]
200+
public async Task FindBindAsyncMethod_FindsSingleArgBindAsync()
201+
{
202+
var type = typeof(BindAsyncSingleArgStruct);
203+
var cache = new ParameterBindingMethodCache();
204+
var parameter = new MockParameterInfo(type, "bindAsyncSingleArgStruct");
205+
var methodFound = cache.FindBindAsyncMethod(parameter);
206+
207+
Assert.NotNull(methodFound.Expression);
208+
Assert.Equal(1, methodFound.ParamCount);
209+
210+
var parsedValue = Expression.Variable(type, "parsedValue");
211+
212+
var parseHttpContext = Expression.Lambda<Func<HttpContext, ValueTask<object>>>(
213+
Expression.Block(new[] { parsedValue }, methodFound.Expression!),
214+
ParameterBindingMethodCache.HttpContextExpr).Compile();
215+
216+
var httpContext = new DefaultHttpContext
217+
{
218+
Request =
219+
{
220+
Headers =
221+
{
222+
["ETag"] = "42",
223+
},
224+
},
225+
};
226+
227+
Assert.Equal(new BindAsyncSingleArgStruct(42), await parseHttpContext(httpContext));
228+
}
229+
198230
public static IEnumerable<object[]> BindAsyncParameterInfoData
199231
{
200232
get
@@ -209,6 +241,14 @@ public static IEnumerable<object[]> BindAsyncParameterInfoData
209241
{
210242
GetFirstParameter((BindAsyncStruct arg) => BindAsyncStructMethod(arg)),
211243
},
244+
new[]
245+
{
246+
GetFirstParameter((BindAsyncSingleArgRecord arg) => BindAsyncSingleArgRecordMethod(arg)),
247+
},
248+
new[]
249+
{
250+
GetFirstParameter((BindAsyncSingleArgStruct arg) => BindAsyncSingleArgStructMethod(arg)),
251+
}
212252
};
213253
}
214254
}
@@ -250,6 +290,11 @@ private static void BindAsyncStructMethod(BindAsyncStruct arg) { }
250290
private static void BindAsyncNullableStructMethod(BindAsyncStruct? arg) { }
251291
private static void NullableReturningBindAsyncStructMethod(NullableReturningBindAsyncStruct arg) { }
252292

293+
private static void BindAsyncSingleArgRecordMethod(BindAsyncSingleArgRecord arg) { }
294+
private static void BindAsyncSingleArgStructMethod(BindAsyncSingleArgStruct arg) { }
295+
private static void BindAsyncNullableSingleArgStructMethod(BindAsyncSingleArgStruct? arg) { }
296+
private static void NullableReturningBindAsyncSingleArgStructMethod(NullableReturningBindAsyncSingleArgStruct arg) { }
297+
253298
private static ParameterInfo GetFirstParameter<T>(Expression<Action<T>> expr)
254299
{
255300
var mc = (MethodCallExpression)expr.Body;
@@ -324,6 +369,38 @@ private record struct NullableReturningBindAsyncStruct(int Value)
324369
throw new NotImplementedException();
325370
}
326371

372+
private record BindAsyncSingleArgRecord(int Value)
373+
{
374+
public static ValueTask<BindAsyncSingleArgRecord?> BindAsync(HttpContext context)
375+
{
376+
if (!int.TryParse(context.Request.Headers.ETag, out var val))
377+
{
378+
return new(result: null);
379+
}
380+
381+
return new(result: new(val));
382+
}
383+
}
384+
385+
private record struct BindAsyncSingleArgStruct(int Value)
386+
{
387+
public static ValueTask<BindAsyncSingleArgStruct> BindAsync(HttpContext context)
388+
{
389+
if (!int.TryParse(context.Request.Headers.ETag, out var val))
390+
{
391+
throw new BadHttpRequestException("The request is missing the required ETag header.");
392+
}
393+
394+
return new(result: new(val));
395+
}
396+
}
397+
398+
private record struct NullableReturningBindAsyncSingleArgStruct(int Value)
399+
{
400+
public static ValueTask<NullableReturningBindAsyncStruct?> BindAsync(HttpContext context, ParameterInfo parameter) =>
401+
throw new NotImplementedException();
402+
}
403+
327404
private class MockParameterInfo : ParameterInfo
328405
{
329406
public MockParameterInfo(Type type, string name)

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

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,54 @@ public static async ValueTask<MyAwaitedBindAsyncStruct> BindAsync(HttpContext co
612612
}
613613
}
614614

615+
private record struct MyBothBindAsyncStruct(Uri Uri)
616+
{
617+
public static ValueTask<MyBothBindAsyncStruct> BindAsync(HttpContext context, ParameterInfo parameter)
618+
{
619+
Assert.True(parameter.ParameterType == typeof(MyBothBindAsyncStruct) || parameter.ParameterType == typeof(MyBothBindAsyncStruct?));
620+
Assert.Equal("myBothBindAsyncStruct", parameter.Name);
621+
622+
if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri))
623+
{
624+
throw new BadHttpRequestException("The request is missing the required Referer header.");
625+
}
626+
627+
return new(result: new(uri));
628+
}
629+
630+
// BindAsync with ParameterInfo is preferred
631+
public static ValueTask<MyBothBindAsyncStruct> BindAsync(HttpContext context)
632+
{
633+
throw new NotImplementedException();
634+
}
635+
}
636+
637+
private record struct MySimpleBindAsyncStruct(Uri Uri)
638+
{
639+
public static ValueTask<MySimpleBindAsyncStruct> BindAsync(HttpContext context)
640+
{
641+
if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri))
642+
{
643+
throw new BadHttpRequestException("The request is missing the required Referer header.");
644+
}
645+
646+
return new(result: new(uri));
647+
}
648+
}
649+
650+
private record MySimpleBindAsyncRecord(Uri Uri)
651+
{
652+
public static ValueTask<MySimpleBindAsyncRecord?> BindAsync(HttpContext context)
653+
{
654+
if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri))
655+
{
656+
return new(result: null);
657+
}
658+
659+
return new(result: new(uri));
660+
}
661+
}
662+
615663
[Theory]
616664
[MemberData(nameof(TryParsableParameters))]
617665
public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue(Delegate action, string? routeValue, object? expectedParameterValue)
@@ -724,6 +772,24 @@ public async Task RequestDelegateUsesBindAsyncOverTryParseGivenNullableStruct()
724772
Assert.Equal(new MyBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBindAsyncStruct"]);
725773
}
726774

775+
[Fact]
776+
public async Task RequestDelegateUsesParameterInfoBindAsyncOverOtherBindAsync()
777+
{
778+
var httpContext = CreateHttpContext();
779+
780+
httpContext.Request.Headers.Referer = "https://example.org";
781+
782+
var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBothBindAsyncStruct? myBothBindAsyncStruct) =>
783+
{
784+
httpContext.Items["myBothBindAsyncStruct"] = myBothBindAsyncStruct;
785+
});
786+
787+
var requestDelegate = resultFactory.RequestDelegate;
788+
await requestDelegate(httpContext);
789+
790+
Assert.Equal(new MyBothBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBothBindAsyncStruct"]);
791+
}
792+
727793
[Fact]
728794
public async Task RequestDelegateUsesTryParseOverBindAsyncGivenExplicitAttribute()
729795
{
@@ -873,7 +939,7 @@ void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2)
873939
[Fact]
874940
public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response()
875941
{
876-
// Not supplying any headers will cause the HttpContext TryParse overload to fail.
942+
// Not supplying any headers will cause the HttpContext BindAsync overload to return null.
877943
var httpContext = CreateHttpContext();
878944
var invoked = false;
879945

@@ -905,7 +971,7 @@ public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response()
905971
[Fact]
906972
public async Task RequestDelegateLogsBindAsyncFailuresAndThrowsIfThrowOnBadRequest()
907973
{
908-
// Not supplying any headers will cause the HttpContext TryParse overload to fail.
974+
// Not supplying any headers will cause the HttpContext BindAsync overload to return null.
909975
var httpContext = CreateHttpContext();
910976
var invoked = false;
911977

@@ -931,10 +997,72 @@ public async Task RequestDelegateLogsBindAsyncFailuresAndThrowsIfThrowOnBadReque
931997
Assert.Equal(400, badHttpRequestException.StatusCode);
932998
}
933999

1000+
[Fact]
1001+
public async Task RequestDelegateLogsSingleArgBindAsyncFailuresAndSets400Response()
1002+
{
1003+
// Not supplying any headers will cause the HttpContext BindAsync overload to return null.
1004+
var httpContext = CreateHttpContext();
1005+
var invoked = false;
1006+
1007+
var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1,
1008+
MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) =>
1009+
{
1010+
invoked = true;
1011+
});
1012+
1013+
var requestDelegate = factoryResult.RequestDelegate;
1014+
await requestDelegate(httpContext);
1015+
1016+
Assert.False(invoked);
1017+
Assert.False(httpContext.RequestAborted.IsCancellationRequested);
1018+
Assert.Equal(400, httpContext.Response.StatusCode);
1019+
1020+
var logs = TestSink.Writes.ToArray();
1021+
1022+
Assert.Equal(2, logs.Length);
1023+
1024+
Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId);
1025+
Assert.Equal(LogLevel.Debug, logs[0].LogLevel);
1026+
Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[0].Message);
1027+
1028+
Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId);
1029+
Assert.Equal(LogLevel.Debug, logs[1].LogLevel);
1030+
Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord2"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[1].Message);
1031+
}
1032+
1033+
[Fact]
1034+
public async Task RequestDelegateLogsSingleArgBindAsyncFailuresAndThrowsIfThrowOnBadRequest()
1035+
{
1036+
// Not supplying any headers will cause the HttpContext BindAsync overload to return null.
1037+
var httpContext = CreateHttpContext();
1038+
var invoked = false;
1039+
1040+
var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1,
1041+
MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) =>
1042+
{
1043+
invoked = true;
1044+
}, new() { ThrowOnBadRequest = true });
1045+
1046+
var requestDelegate = factoryResult.RequestDelegate;
1047+
var badHttpRequestException = await Assert.ThrowsAsync<BadHttpRequestException>(() => requestDelegate(httpContext));
1048+
1049+
Assert.False(invoked);
1050+
1051+
// The httpContext should be untouched.
1052+
Assert.False(httpContext.RequestAborted.IsCancellationRequested);
1053+
Assert.Equal(200, httpContext.Response.StatusCode);
1054+
Assert.False(httpContext.Response.HasStarted);
1055+
1056+
// We don't log bad requests when we throw.
1057+
Assert.Empty(TestSink.Writes);
1058+
1059+
Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", badHttpRequestException.Message);
1060+
Assert.Equal(400, badHttpRequestException.StatusCode);
1061+
}
1062+
9341063
[Fact]
9351064
public async Task BindAsyncExceptionsAreUncaught()
9361065
{
937-
// Not supplying any headers will cause the HttpContext BindAsync overload to fail.
9381066
var httpContext = CreateHttpContext();
9391067

9401068
var factoryResult = RequestDelegateFactory.Create((MyBindAsyncTypeThatThrows arg1) => { });
@@ -2239,6 +2367,10 @@ void nullableReferenceType(HttpContext context, MyBindAsyncRecord? myBindAsyncRe
22392367
{
22402368
context.Items["uri"] = myBindAsyncRecord?.Uri;
22412369
}
2370+
void requiredReferenceTypeSimple(HttpContext context, MySimpleBindAsyncRecord mySimpleBindAsyncRecord)
2371+
{
2372+
context.Items["uri"] = mySimpleBindAsyncRecord.Uri;
2373+
}
22422374

22432375

22442376
void requiredValueType(HttpContext context, MyNullableBindAsyncStruct myNullableBindAsyncStruct)
@@ -2253,11 +2385,16 @@ void nullableValueType(HttpContext context, MyNullableBindAsyncStruct? myNullabl
22532385
{
22542386
context.Items["uri"] = myNullableBindAsyncStruct?.Uri;
22552387
}
2388+
void requiredValueTypeSimple(HttpContext context, MySimpleBindAsyncStruct mySimpleBindAsyncStruct)
2389+
{
2390+
context.Items["uri"] = mySimpleBindAsyncStruct.Uri;
2391+
}
22562392

22572393
return new object?[][]
22582394
{
22592395
new object?[] { (Action<HttpContext, MyBindAsyncRecord>)requiredReferenceType, false, true, false },
22602396
new object?[] { (Action<HttpContext, MyBindAsyncRecord>)requiredReferenceType, true, false, false, },
2397+
new object?[] { (Action<HttpContext, MySimpleBindAsyncRecord>)requiredReferenceTypeSimple, true, false, false },
22612398

22622399
new object?[] { (Action<HttpContext, MyBindAsyncRecord?>)defaultReferenceType, false, false, false, },
22632400
new object?[] { (Action<HttpContext, MyBindAsyncRecord?>)defaultReferenceType, true, false, false },
@@ -2267,6 +2404,7 @@ void nullableValueType(HttpContext context, MyNullableBindAsyncStruct? myNullabl
22672404

22682405
new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct>)requiredValueType, false, true, true },
22692406
new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct>)requiredValueType, true, false, true },
2407+
new object?[] { (Action<HttpContext, MySimpleBindAsyncStruct>)requiredValueTypeSimple, true, false, true },
22702408

22712409
new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct?>)defaultValueType, false, false, true },
22722410
new object?[] { (Action<HttpContext, MyNullableBindAsyncStruct?>)defaultValueType, true, false, true },

0 commit comments

Comments
 (0)