Skip to content
211 changes: 210 additions & 1 deletion src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Numerics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
Expand Down Expand Up @@ -472,6 +474,123 @@ private static Expression AddResponseWritingToMethodCall(Expression methodCall,
};
}

private static MethodInfo GetEnumTryParseMethod()
{
var staticEnumMethods = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.IsGenericMethod && m.Name == "TryParse" && m.ReturnType == typeof(bool));

foreach (var method in staticEnumMethods)
{
var tryParseParameters = method.GetParameters();

if (tryParseParameters.Length == 2 &&
tryParseParameters[0].ParameterType == typeof(string) &&
tryParseParameters[1].IsOut)
{
return method;
}
}

throw new Exception("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) does not exist!!?!?");
}

private static MethodInfo GetDateTimeTryPareMethod(Type type)
{
if (type != typeof(DateTime) && type != typeof(DateOnly) &&
type != typeof(DateTimeOffset) && type != typeof(TimeOnly))
{
throw new Exception("Parameter is not of type of DateTime, DateOnly, DateTimeOffset, TimeOnly !");
}

var staticDateMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.Name == "TryParse" && m.ReturnType == typeof(bool))
.OrderByDescending(m => m.GetParameters().Length);

foreach (var method in staticDateMethods)
{
var tryParseParameters = method.GetParameters();

if (tryParseParameters.Length == 4 &&
tryParseParameters[0].ParameterType == typeof(string) &&
tryParseParameters[1].ParameterType == typeof(IFormatProvider) &&
tryParseParameters[2].ParameterType == typeof(DateTimeStyles) &&
tryParseParameters[3].IsOut &&
tryParseParameters[3].ParameterType == type.MakeByRefType())
{
return method;
}
}

throw new Exception("static bool TryParse(string?, IFormatProvider, DateTimeStyles, out DateTime result) does not exit!!?!?");
}

// TODO: Use InvariantCulture where possible? Or is CurrentCulture fine because it's more flexible?
internal static MethodInfo? FindTryParseMethod(Type type)
{
static MethodInfo? Finder(Type type)
{
if (type.IsEnum)
{
return EnumTryParseMethod.MakeGenericMethod(type);
}

if (UseTryParseWithDateTimeStyleOptions(type))
{
return GetDateTimeTryPareMethod(type);
}

bool useNumberStyle = UseTryParseWithNumberStyleOption(type);

var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.Name == "TryParse" && m.ReturnType == typeof(bool))
.OrderByDescending(m => m.GetParameters().Length);

foreach (var method in staticMethods)
{
var tryParseParameters = method.GetParameters();

if (useNumberStyle && tryParseParameters.Length == 4 &&
tryParseParameters[0].ParameterType == typeof(string) &&
tryParseParameters[1].ParameterType == typeof(NumberStyles) &&
tryParseParameters[2].ParameterType == typeof(IFormatProvider) &&
tryParseParameters[3].IsOut &&
tryParseParameters[3].ParameterType == type.MakeByRefType())
{
return method;
}
else if (tryParseParameters.Length == 3 &&
tryParseParameters[0].ParameterType == typeof(string) &&
tryParseParameters[1].ParameterType == typeof(IFormatProvider) &&
tryParseParameters[2].IsOut &&
tryParseParameters[2].ParameterType == type.MakeByRefType())
{
return method;
}
else if (tryParseParameters.Length == 2 &&
tryParseParameters[0].ParameterType == typeof(string) &&
tryParseParameters[1].IsOut &&
tryParseParameters[1].ParameterType == type.MakeByRefType())
{
return method;
}
else
{
continue;
}
}

return null;
}

return TryParseMethodCache.GetOrAdd(type, Finder);
}

private static bool HasTryParseMethod(ParameterInfo parameter)
{
var nonNullableParameterType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
return FindTryParseMethod(nonNullableParameterType) is not null;
}

private static Expression GetValueFromProperty(Expression sourceExpression, string key)
{
var itemProperty = sourceExpression.Type.GetProperty("Item");
Expand Down Expand Up @@ -560,7 +679,47 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
Expression.Call(LogParameterBindingFailureMethod,
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr));

var tryParseCall = Expression.Call(tryParseMethod, TempSourceStringExpr, parsedValue);
// Before call the TryParse Method, we should know the exact param to inject
Type type = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
var useNumberStyles = UseTryParseWithNumberStyleOption(type);
var useDateTimeStyles = UseTryParseWithDateTimeStyleOptions(type);

MethodCallExpression? tryParseCall;

// Should we use CultureInvariant
if (tryParseMethod.GetParameters().Length > 2)
{
if (useNumberStyles)
{
tryParseCall = Expression.Call(
tryParseMethod,
TempSourceStringExpr,
Expression.Constant(SetRightNumberStyles(type)),
Expression.Constant(CultureInfo.InvariantCulture),
parsedValue);
}
else if (useDateTimeStyles)
{
tryParseCall = Expression.Call(
tryParseMethod,
TempSourceStringExpr,
Expression.Constant(CultureInfo.InvariantCulture),
Expression.Constant(DateTimeStyles.None),
parsedValue);
}
else
{
tryParseCall = Expression.Call(
tryParseMethod,
TempSourceStringExpr,
Expression.Constant(CultureInfo.InvariantCulture),
parsedValue);
}
}
else
{
tryParseCall = Expression.Call(tryParseMethod, TempSourceStringExpr, parsedValue);
}

// If the parameter is nullable, we need to assign the "parsedValue" local to the nullable parameter on success.
Expression tryParseExpression = isNotNullable ?
Expand Down Expand Up @@ -610,6 +769,56 @@ private static Expression BindParameterFromBody(Type parameterType, bool allowEm
return Expression.Convert(BodyValueExpr, parameterType);
}

private static bool UseTryParseWithNumberStyleOption(Type type)
=> type == typeof(int) ||
type == typeof(double) ||
type == typeof(decimal) ||
type == typeof(float) ||
type == typeof(Half) ||
type == typeof(short) ||
type == typeof(long) ||
type == typeof(IntPtr) ||
type == typeof(byte) ||
type == typeof(sbyte) ||
type == typeof(ushort) ||
type == typeof(uint) ||
type == typeof(ulong) ||
type == typeof(BigInteger);

private static bool UseTryParseWithDateTimeStyleOptions(Type type)
=> type == typeof(DateTime) ||
type == typeof(DateTimeOffset) ||
type == typeof(DateOnly) ||
type == typeof(TimeOnly);

private static NumberStyles SetRightNumberStyles(Type type)
{
if (!UseTryParseWithNumberStyleOption(type))
{
throw new InvalidOperationException("Incorrect type !");
}

if (type == typeof(int) || type == typeof(short) || type == typeof(IntPtr) ||
type == typeof(long) || type == typeof(byte) || type == typeof(sbyte) ||
type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong) ||
type == typeof(BigInteger))
{
return NumberStyles.Integer;
}

if (type == typeof(double) || type == typeof(float) || type == typeof(Half))
{
return NumberStyles.AllowThousands | NumberStyles.Float;
}

if (type == typeof(decimal))
{
return NumberStyles.Number;
}

return NumberStyles.None;
}

private static MethodInfo GetMethodInfo<T>(Expression<T> expr)
{
var mc = (MethodCallExpression)expr.Body;
Expand Down
59 changes: 59 additions & 0 deletions src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;

using Xunit;

namespace Microsoft.AspNetCore.Routing.Internal
Expand Down Expand Up @@ -473,6 +475,63 @@ public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMe
Assert.Equal("No public static bool Object.TryParse(string, out Object) method found for notTryParsable.", ex.Message);
}

[Theory]
[InlineData(typeof(int))]
[InlineData(typeof(double))]
[InlineData(typeof(float))]
[InlineData(typeof(Half))]
[InlineData(typeof(short))]
[InlineData(typeof(long))]
[InlineData(typeof(IntPtr))]
[InlineData(typeof(sbyte))]
[InlineData(typeof(ushort))]
[InlineData(typeof(uint))]
[InlineData(typeof(ulong))]
public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCulture(Type @type)
{
var methodFound = RequestDelegateFactory.FindTryParseMethod(@type);

Assert.NotNull(methodFound);

var parameters = methodFound!.GetParameters();
Assert.Equal(4, parameters.Length);
Assert.Equal(typeof(string), parameters[0].ParameterType);
Assert.Equal(typeof(NumberStyles), parameters[1].ParameterType);
Assert.Equal(typeof(IFormatProvider), parameters[2].ParameterType);
Assert.True(parameters[3].IsOut);
}

[Theory]
[InlineData(typeof(DateTime))]
[InlineData(typeof(DateOnly))]
[InlineData(typeof(DateTimeOffset))]
[InlineData(typeof(TimeOnly))]
[InlineData(typeof(TimeSpan))]
public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCultureDateType(Type @type)
{
var methodFound = RequestDelegateFactory.FindTryParseMethod(@type);

Assert.NotNull(methodFound);

var parameters = methodFound!.GetParameters();

if (@type == typeof(TimeSpan))
{
Assert.Equal(3, parameters.Length);
Assert.Equal(typeof(string), parameters[0].ParameterType);
Assert.Equal(typeof(IFormatProvider), parameters[1].ParameterType);
Assert.True(parameters[2].IsOut);
}
else
{
Assert.Equal(4, parameters.Length);
Assert.Equal(typeof(string), parameters[0].ParameterType);
Assert.Equal(typeof(IFormatProvider), parameters[1].ParameterType);
Assert.Equal(typeof(DateTimeStyles), parameters[2].ParameterType);
Assert.True(parameters[3].IsOut);
}
}

[Fact]
public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument()
{
Expand Down