Skip to content

Commit 8ae29f4

Browse files
authored
Produce more efficient TryParse code gen for minimal actions (#31739)
* Produce more efficient TryParse code gen. * Fix optional string parameter handling * Remove innerTempSourceString * Update comments * Add test cases for IEnumerable<TService> * fix tests
1 parent 5e71c23 commit 8ae29f4

File tree

2 files changed

+163
-84
lines changed

2 files changed

+163
-84
lines changed

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

Lines changed: 109 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
67
using System.IO;
78
using System.Linq;
89
using System.Linq.Expressions;
@@ -53,6 +54,8 @@ public static class RequestDelegateFactory
5354
private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, nameof(HttpResponse.StatusCode));
5455
private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask));
5556

57+
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
58+
5659
private static readonly ConcurrentDictionary<Type, MethodInfo?> TryParseMethodCache = new();
5760

5861
/// <summary>
@@ -155,10 +158,15 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func<HttpContext, ob
155158

156159
var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext);
157160

158-
var responseWritingMethodCall = factoryContext.CheckForTryParseFailure ?
159-
CreateTryParseCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments) :
161+
var responseWritingMethodCall = factoryContext.TryParseParams.Count > 0 ?
162+
CreateTryParseCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments, factoryContext) :
160163
CreateResponseWritingMethodCall(methodInfo, targetExpression, arguments);
161164

165+
if (factoryContext.UsingTempSourceString)
166+
{
167+
responseWritingMethodCall = Expression.Block(new[] { TempSourceStringExpr }, responseWritingMethodCall);
168+
}
169+
162170
return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext);
163171
}
164172

@@ -242,30 +250,30 @@ private static Expression CreateResponseWritingMethodCall(MethodInfo methodInfo,
242250
}
243251

244252
// If we're calling TryParse and wasTryParseFailure indicates it failed, set a 400 StatusCode instead of calling the method.
245-
private static Expression CreateTryParseCheckingResponseWritingMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments)
253+
private static Expression CreateTryParseCheckingResponseWritingMethodCall(
254+
MethodInfo methodInfo, Expression? target, Expression[] arguments, FactoryContext factoryContext)
246255
{
247256
// {
248-
// bool wasTryParseFailure = false;
249257
// string tempSourceString;
258+
// bool wasTryParseFailure = false;
250259
//
251-
// // Assume "[FromRoute] int id" is the first parameter.
252-
//
253-
// tempSourceString = httpContext.RequestValue["id"];
254-
// int param1 = tempSourceString == null ? default :
255-
// {
256-
// int parsedValue = default;
260+
// // Assume "int param1" is the first parameter, "[FromRoute] int? param2 = 42" is the second parameter ...
261+
// int param1_local;
262+
// int? param2_local;
263+
// // ...
257264
//
258-
// if (!int.TryParse(tempSourceString, out parsedValue))
259-
// {
260-
// wasTryParseFailure = true;
261-
// Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString)
262-
// }
265+
// tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"];
263266
//
264-
// return parsedValue;
265-
// };
267+
// if (tempSourceString != null)
268+
// {
269+
// if (!int.TryParse(tempSourceString, out param1_local))
270+
// {
271+
// wasTryParseFailure = true;
272+
// Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString)
273+
// }
274+
// }
266275
//
267-
// tempSourceString = httpContext.RequestValue["param2"];
268-
// int param2 = tempSourceString == null ? default :
276+
// tempSourceString = httpContext.RouteValue["param2"];
269277
// // ...
270278
//
271279
// return wasTryParseFailure ?
@@ -274,41 +282,33 @@ private static Expression CreateTryParseCheckingResponseWritingMethodCall(Method
274282
// return Task.CompletedTask;
275283
// } :
276284
// {
277-
// // Logic generated by AddResponseWritingToMethodCall() that calls action(param1, parm2, ...)
285+
// // Logic generated by AddResponseWritingToMethodCall() that calls action(param1_local, param2_local, ...)
278286
// };
279287
// }
280288

281-
var parameters = methodInfo.GetParameters();
282-
var storedArguments = new ParameterExpression[parameters.Length];
283-
var localVariables = new ParameterExpression[parameters.Length + 2];
289+
var localVariables = new ParameterExpression[factoryContext.TryParseParams.Count + 1];
290+
var tryParseAndCallMethod = new Expression[factoryContext.TryParseParams.Count + 1];
284291

285-
for (var i = 0; i < parameters.Length; i++)
292+
for (var i = 0; i < factoryContext.TryParseParams.Count; i++)
286293
{
287-
storedArguments[i] = localVariables[i] = Expression.Parameter(parameters[i].ParameterType);
294+
(localVariables[i], tryParseAndCallMethod[i]) = factoryContext.TryParseParams[i];
288295
}
289296

290-
localVariables[parameters.Length] = WasTryParseFailureExpr;
291-
localVariables[parameters.Length + 1] = TempSourceStringExpr;
292-
293-
var assignAndCall = new Expression[parameters.Length + 1];
294-
for (var i = 0; i < parameters.Length; i++)
295-
{
296-
assignAndCall[i] = Expression.Assign(localVariables[i], arguments[i]);
297-
}
297+
localVariables[factoryContext.TryParseParams.Count] = WasTryParseFailureExpr;
298298

299299
var set400StatusAndReturnCompletedTask = Expression.Block(
300300
Expression.Assign(StatusCodeExpr, Expression.Constant(400)),
301301
CompletedTaskExpr);
302302

303-
var methodCall = CreateMethodCall(methodInfo, target, storedArguments);
303+
var methodCall = CreateMethodCall(methodInfo, target, arguments);
304304

305305
var checkWasTryParseFailure = Expression.Condition(WasTryParseFailureExpr,
306306
set400StatusAndReturnCompletedTask,
307307
AddResponseWritingToMethodCall(methodCall, methodInfo.ReturnType));
308308

309-
assignAndCall[parameters.Length] = checkWasTryParseFailure;
309+
tryParseAndCallMethod[factoryContext.TryParseParams.Count] = checkWasTryParseFailure;
310310

311-
return Expression.Block(localVariables, assignAndCall);
311+
return Expression.Block(localVariables, tryParseAndCallMethod);
312312
}
313313

314314
private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType)
@@ -540,10 +540,24 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
540540
{
541541
if (parameter.ParameterType == typeof(string))
542542
{
543-
return valueExpression;
543+
if (!parameter.HasDefaultValue)
544+
{
545+
return valueExpression;
546+
}
547+
548+
factoryContext.UsingTempSourceString = true;
549+
return Expression.Block(
550+
Expression.Assign(TempSourceStringExpr, valueExpression),
551+
Expression.Condition(TempSourceStringNotNullExpr,
552+
TempSourceStringExpr,
553+
Expression.Constant(parameter.DefaultValue)));
544554
}
545555

556+
factoryContext.UsingTempSourceString = true;
557+
546558
var underlyingNullableType = Nullable.GetUnderlyingType(parameter.ParameterType);
559+
var isNotNullable = underlyingNullableType is null;
560+
547561
var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType;
548562
var tryParseMethod = FindTryParseMethod(nonNullableParameterType);
549563

@@ -552,28 +566,47 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
552566
throw new InvalidOperationException($"No public static bool {parameter.ParameterType.Name}.TryParse(string, out {parameter.ParameterType.Name}) method found for {parameter.Name}.");
553567
}
554568

555-
// bool wasTryParseFailure = false;
556569
// string tempSourceString;
570+
// bool wasTryParseFailure = false;
557571
//
558-
// // Assume "[FromRoute] int id" is the first parameter.
559-
// tempSourceString = httpContext.RequestValue["id"];
572+
// // Assume "int param1" is the first parameter and "[FromRoute] int? param2 = 42" is the second parameter.
573+
// int param1_local;
574+
// int? param2_local;
560575
//
561-
// int param1 = tempSourceString == null ? default :
576+
// tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"];
577+
//
578+
// if (tempSourceString != null)
562579
// {
563-
// int parsedValue = default;
580+
// if (!int.TryParse(tempSourceString, out param1_local))
581+
// {
582+
// wasTryParseFailure = true;
583+
// Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString)
584+
// }
585+
// }
564586
//
565-
// if (!int.TryParse(tempSourceString, out parsedValue))
566-
// {
567-
// wasTryParseFailure = true;
568-
// Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString)
569-
// }
587+
// tempSourceString = httpContext.RouteValue["param2"];
570588
//
571-
// return parsedValue;
572-
// };
589+
// if (tempSourceString != null)
590+
// {
591+
// if (int.TryParse(tempSourceString, out int parsedValue))
592+
// {
593+
// param2_local = parsedValue;
594+
// }
595+
// else
596+
// {
597+
// wasTryParseFailure = true;
598+
// Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString)
599+
// }
600+
// }
601+
// else
602+
// {
603+
// param2_local = 42;
604+
// }
573605

574-
factoryContext.CheckForTryParseFailure = true;
606+
var argument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");
575607

576-
var parsedValue = Expression.Variable(nonNullableParameterType);
608+
// If the parameter is nullable, create a "parsedValue" local to TryParse into since we cannot the parameter directly.
609+
var parsedValue = isNotNullable ? argument : Expression.Variable(nonNullableParameterType, "parsedValue");
577610

578611
var parameterTypeNameConstant = Expression.Constant(parameter.ParameterType.Name);
579612
var parameterNameConstant = Expression.Constant(parameter.Name);
@@ -584,32 +617,30 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
584617
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr));
585618

586619
var tryParseCall = Expression.Call(tryParseMethod, TempSourceStringExpr, parsedValue);
587-
var ifFailExpression = Expression.IfThen(Expression.Not(tryParseCall), failBlock);
588-
589-
Expression parsedValueExpression = Expression.Block(new[] { parsedValue },
590-
ifFailExpression,
591-
parsedValue);
592-
593-
// Convert back to nullable if necessary.
594-
if (underlyingNullableType is not null)
595-
{
596-
parsedValueExpression = Expression.Convert(parsedValueExpression, parameter.ParameterType);
597-
}
598-
599-
Expression defaultExpression = parameter.HasDefaultValue ?
600-
Expression.Constant(parameter.DefaultValue) :
601-
Expression.Default(parameter.ParameterType);
602-
603-
// tempSourceString = httpContext.RequestValue["id"];
604-
var storeValueToTemp = Expression.Assign(TempSourceStringExpr, valueExpression);
605-
606-
// int param1 = tempSourcString == null ? default : ...
607-
var ternary = Expression.Condition(
608-
Expression.Equal(TempSourceStringExpr, Expression.Constant(null)),
609-
defaultExpression,
610-
parsedValueExpression);
611620

612-
return Expression.Block(storeValueToTemp, ternary);
621+
// If the parameter is nullable, we need to assign the "parsedValue" local to the nullable parameter on success.
622+
Expression tryParseExpression = isNotNullable ?
623+
Expression.IfThen(Expression.Not(tryParseCall), failBlock) :
624+
Expression.Block(new[] { parsedValue },
625+
Expression.IfThenElse(tryParseCall,
626+
Expression.Assign(argument, Expression.Convert(parsedValue, parameter.ParameterType)),
627+
failBlock));
628+
629+
var ifNotNullTryParse = !parameter.HasDefaultValue ?
630+
Expression.IfThen(TempSourceStringNotNullExpr, tryParseExpression) :
631+
Expression.IfThenElse(TempSourceStringNotNullExpr,
632+
tryParseExpression,
633+
Expression.Assign(argument, Expression.Constant(parameter.DefaultValue)));
634+
635+
var fullTryParseBlock = Expression.Block(
636+
// tempSourceString = httpContext.RequestValue["id"];
637+
Expression.Assign(TempSourceStringExpr, valueExpression),
638+
// if (tempSourceString != null) { ... }
639+
ifNotNullTryParse);
640+
641+
factoryContext.TryParseParams.Add((argument, fullTryParseBlock));
642+
643+
return argument;
613644
}
614645

615646
private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext) =>
@@ -747,7 +778,8 @@ private class FactoryContext
747778
public Type? JsonRequestBodyType { get; set; }
748779
public bool AllowEmptyRequestBody { get; set; }
749780

750-
public bool CheckForTryParseFailure { get; set; }
781+
public bool UsingTempSourceString { get; set; }
782+
public List<(ParameterExpression, Expression)> TryParseParams { get; } = new();
751783
}
752784

753785
private static class Log

0 commit comments

Comments
 (0)