Skip to content

Commit 924b8e8

Browse files
authored
Add support for TryParse(HttpContext, ...) to RequestDelegateFactory (#35433)
1 parent 61fc66c commit 924b8e8

File tree

6 files changed

+491
-49
lines changed

6 files changed

+491
-49
lines changed

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

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Linq;
56
using System.Linq.Expressions;
67
using System.Reflection;
@@ -35,17 +36,19 @@ public static partial class RequestDelegateFactory
3536
private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!;
3637
private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
3738
private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default));
38-
private static readonly MethodInfo LogParameterBindingFailureMethod = GetMethodInfo<Action<HttpContext, string, string, string>>((httpContext, parameterType, parameterName, sourceValue) =>
39-
Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue));
4039

40+
private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo<Action<HttpContext, string, string, string>>((httpContext, parameterType, parameterName, sourceValue) =>
41+
Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue));
4142
private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo<Action<HttpContext, string, string>>((httpContext, parameterType, parameterName) =>
4243
Log.RequiredParameterNotProvided(httpContext, parameterType, parameterName));
44+
private static readonly MethodInfo LogParameterBindingFromHttpContextFailedMethod = GetMethodInfo<Action<HttpContext, string, string>>((httpContext, parameterType, parameterName) =>
45+
Log.ParameterBindingFromHttpContextFailed(httpContext, parameterType, parameterName));
4346

4447
private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target");
45-
private static readonly ParameterExpression HttpContextExpr = Expression.Parameter(typeof(HttpContext), "httpContext");
4648
private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue");
4749
private static readonly ParameterExpression WasParamCheckFailureExpr = Expression.Variable(typeof(bool), "wasParamCheckFailure");
4850

51+
private static ParameterExpression HttpContextExpr => TryParseMethodCache.HttpContextExpr;
4952
private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.RequestServices))!);
5053
private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Request))!);
5154
private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Response))!);
@@ -57,8 +60,9 @@ public static partial class RequestDelegateFactory
5760
private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!);
5861
private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask));
5962

60-
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TryParseMethodCache.TempSourceStringExpr, Expression.Constant(null));
61-
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TryParseMethodCache.TempSourceStringExpr, Expression.Constant(null));
63+
private static ParameterExpression TempSourceStringExpr => TryParseMethodCache.TempSourceStringExpr;
64+
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
65+
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
6266

6367
/// <summary>
6468
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="action"/>.
@@ -170,7 +174,7 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func<HttpContext, ob
170174

171175
if (factoryContext.UsingTempSourceString)
172176
{
173-
responseWritingMethodCall = Expression.Block(new[] { TryParseMethodCache.TempSourceStringExpr }, responseWritingMethodCall);
177+
responseWritingMethodCall = Expression.Block(new[] { TempSourceStringExpr }, responseWritingMethodCall);
174178
}
175179

176180
return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext);
@@ -259,7 +263,11 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
259263
{
260264
return RequestAbortedExpr;
261265
}
262-
else if (parameter.ParameterType == typeof(string) || TryParseMethodCache.HasTryParseMethod(parameter))
266+
else if (TryParseMethodCache.HasTryParseHttpContextMethod(parameter))
267+
{
268+
return BindParameterFromTryParseHttpContext(parameter, factoryContext);
269+
}
270+
else if (parameter.ParameterType == typeof(string) || TryParseMethodCache.HasTryParseStringMethod(parameter))
263271
{
264272
// 1. We bind from route values only, if route parameters are non-null and the parameter name is in that set.
265273
// 2. We bind from query only, if route parameters are non-null and the parameter name is NOT in that set.
@@ -620,7 +628,7 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
620628
var isNotNullable = underlyingNullableType is null;
621629

622630
var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType;
623-
var tryParseMethodCall = TryParseMethodCache.FindTryParseMethod(nonNullableParameterType);
631+
var tryParseMethodCall = TryParseMethodCache.FindTryParseStringMethod(nonNullableParameterType);
624632

625633
if (tryParseMethodCall is null)
626634
{
@@ -664,16 +672,16 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
664672
// param2_local = 42;
665673
// }
666674

667-
// If the parameter is nullable, create a "parsedValue" local to TryParse into since we cannot the parameter directly.
675+
// If the parameter is nullable, create a "parsedValue" local to TryParse into since we cannot use the parameter directly.
668676
var parsedValue = isNotNullable ? argument : Expression.Variable(nonNullableParameterType, "parsedValue");
669677

670678
var parameterTypeNameConstant = Expression.Constant(parameter.ParameterType.Name);
671679
var parameterNameConstant = Expression.Constant(parameter.Name);
672680

673681
var failBlock = Expression.Block(
674682
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
675-
Expression.Call(LogParameterBindingFailureMethod,
676-
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TryParseMethodCache.TempSourceStringExpr));
683+
Expression.Call(LogParameterBindingFailedMethod,
684+
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr));
677685

678686
var tryParseCall = tryParseMethodCall(parsedValue);
679687

@@ -712,14 +720,14 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
712720
var fullParamCheckBlock = !isOptional
713721
? Expression.Block(
714722
// tempSourceString = httpContext.RequestValue["id"];
715-
Expression.Assign(TryParseMethodCache.TempSourceStringExpr, valueExpression),
723+
Expression.Assign(TempSourceStringExpr, valueExpression),
716724
// if (tempSourceString == null) { ... } only produced when parameter is required
717725
checkRequiredParaseableParameterBlock,
718726
// if (tempSourceString != null) { ... }
719727
ifNotNullTryParse)
720728
: Expression.Block(
721729
// tempSourceString = httpContext.RequestValue["id"];
722-
Expression.Assign(TryParseMethodCache.TempSourceStringExpr, valueExpression),
730+
Expression.Assign(TempSourceStringExpr, valueExpression),
723731
// if (tempSourceString != null) { ... }
724732
ifNotNullTryParse);
725733

@@ -739,6 +747,42 @@ private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo
739747
return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext);
740748
}
741749

750+
private static Expression BindParameterFromTryParseHttpContext(ParameterInfo parameter, FactoryContext factoryContext)
751+
{
752+
// bool wasParamCheckFailure = false;
753+
//
754+
// // Assume "Foo param1" is the first parameter and "public static bool TryParse(HttpContext context, out Foo foo)" exists.
755+
// Foo param1_local;
756+
//
757+
// if (!Foo.TryParse(httpContext, out param1_local))
758+
// {
759+
// wasParamCheckFailure = true;
760+
// Log.ParameterBindingFromHttpContextFailed(httpContext, "Foo", "foo")
761+
// }
762+
763+
var argument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");
764+
var tryParseMethodCall = TryParseMethodCache.FindTryParseHttpContextMethod(parameter.ParameterType);
765+
766+
// There's no way to opt-in to using a TryParse method on HttpContext other than defining the method, so it's guaranteed to exist here.
767+
Debug.Assert(tryParseMethodCall is not null);
768+
769+
var parameterTypeNameConstant = Expression.Constant(parameter.ParameterType.Name);
770+
var parameterNameConstant = Expression.Constant(parameter.Name);
771+
772+
var failBlock = Expression.Block(
773+
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
774+
Expression.Call(LogParameterBindingFromHttpContextFailedMethod,
775+
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant));
776+
777+
var tryParseCall = tryParseMethodCall(argument);
778+
var fullParamCheckBlock = Expression.IfThen(Expression.Not(tryParseCall), failBlock);
779+
780+
factoryContext.ExtraLocals.Add(argument);
781+
factoryContext.ParamCheckExpressions.Add(fullParamCheckBlock);
782+
783+
return argument;
784+
}
785+
742786
private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext)
743787
{
744788
if (factoryContext.JsonRequestBodyType is not null)
@@ -1025,19 +1069,27 @@ public static void RequestBodyInvalidDataException(HttpContext httpContext, Inva
10251069
public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue)
10261070
=> ParameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue);
10271071

1028-
public static void RequiredParameterNotProvided(HttpContext httpContext, string parameterTypeName, string parameterName)
1029-
=> RequiredParameterNotProvided(GetLogger(httpContext), parameterTypeName, parameterName);
1030-
10311072
[LoggerMessage(3, LogLevel.Debug,
10321073
@"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}"".",
10331074
EventName = "ParamaterBindingFailed")]
10341075
private static partial void ParameterBindingFailed(ILogger logger, string parameterType, string parameterName, string sourceValue);
10351076

1077+
public static void RequiredParameterNotProvided(HttpContext httpContext, string parameterTypeName, string parameterName)
1078+
=> RequiredParameterNotProvided(GetLogger(httpContext), parameterTypeName, parameterName);
1079+
10361080
[LoggerMessage(4, LogLevel.Debug,
10371081
@"Required parameter ""{ParameterType} {ParameterName}"" was not provided.",
10381082
EventName = "RequiredParameterNotProvided")]
10391083
private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName);
10401084

1085+
public static void ParameterBindingFromHttpContextFailed(HttpContext httpContext, string parameterTypeName, string parameterName)
1086+
=> ParameterBindingFromHttpContextFailed(GetLogger(httpContext), parameterTypeName, parameterName);
1087+
1088+
[LoggerMessage(5, LogLevel.Debug,
1089+
@"Failed to bind parameter ""{ParameterType} {ParameterName}"" from HttpContext.",
1090+
EventName = "ParameterBindingFromHttpContextFailed")]
1091+
private static partial void ParameterBindingFromHttpContextFailed(ILogger logger, string parameterType, string parameterName);
1092+
10411093
private static ILogger GetLogger(HttpContext httpContext)
10421094
{
10431095
var loggerFactory = httpContext.RequestServices.GetRequiredService<ILoggerFactory>();

0 commit comments

Comments
 (0)