Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
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 @@ -503,9 +505,9 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
var isNotNullable = underlyingNullableType is null;

var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType;
var tryParseMethod = TryParseMethodCache.FindTryParseMethod(nonNullableParameterType);
var tryParseMethodCall = TryParseMethodCache.FindTryParseMethodCall(parameter);

if (tryParseMethod is null)
if (tryParseMethodCall is null)
{
throw new InvalidOperationException($"No public static bool {parameter.ParameterType.Name}.TryParse(string, out {parameter.ParameterType.Name}) method found for {parameter.Name}.");
}
Expand Down Expand Up @@ -560,7 +562,7 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
Expression.Call(LogParameterBindingFailureMethod,
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr));

var tryParseCall = Expression.Call(tryParseMethod, TempSourceStringExpr, parsedValue);
MethodCallExpression? tryParseCall = tryParseMethodCall(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
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
67 changes: 67 additions & 0 deletions src/Http/Http.Extensions/test/TryParseMethodCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Globalization;

using Xunit;

namespace Microsoft.AspNetCore.Http.Extensions.Tests
{
public class TryParseMethodCacheTests
{
[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 = TryParseMethodCache.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 = TryParseMethodCache.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);
}
}
}
}
216 changes: 210 additions & 6 deletions src/Shared/TryParseMethodCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Numerics;
using System.Reflection;

namespace Microsoft.AspNetCore.Http
Expand All @@ -13,12 +17,11 @@ internal static class TryParseMethodCache
private static readonly MethodInfo EnumTryParseMethod = GetEnumTryParseMethod();

// Since this is shared source, the cache won't be shared between RequestDelegateFactory and the ApiDescriptionProvider sadly :(
private static readonly ConcurrentDictionary<Type, MethodInfo?> Cache = new();
private static readonly ConcurrentDictionary<Type, Func<ParameterExpression, Expression, MethodCallExpression>?> MethodCallCache = new();

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

// TODO: Use InvariantCulture where possible? Or is CurrentCulture fine because it's more flexible?
Expand All @@ -31,7 +34,18 @@ public static bool HasTryParseMethod(ParameterInfo parameter)
return EnumTryParseMethod.MakeGenericMethod(type);
}

var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static);
if (TryGetDateTimeTryPareMethod(type, out var methodDateInfo))
{
return methodDateInfo;
}

if (TryGetNumberStylesTryGetMethod(type, out var methodInfo, out var _))
{
return methodInfo;
}

var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.OrderByDescending(m => m.GetParameters().Length); ;

foreach (var method in staticMethods)
{
Expand All @@ -42,7 +56,15 @@ public static bool HasTryParseMethod(ParameterInfo parameter)

var tryParseParameters = method.GetParameters();

if (tryParseParameters.Length == 2 &&
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())
Expand All @@ -54,7 +76,84 @@ public static bool HasTryParseMethod(ParameterInfo parameter)
return null;
}

return Cache.GetOrAdd(type, Finder);
return Finder(type);
}

public static Func<ParameterExpression, Expression, MethodCallExpression>? FindTryParseMethodCall(ParameterInfo parameter)
{
static Func<ParameterExpression, Expression, MethodCallExpression>? Finder(Type type)
{
MethodInfo? methodInfo;

if (type.IsEnum)
{
methodInfo = EnumTryParseMethod.MakeGenericMethod(type);
if (methodInfo != null)
{
return (parameterExpression, expression) => Expression.Call(methodInfo!, parameterExpression, expression);
}

return null;
}

if (TryGetDateTimeTryPareMethod(type, out methodInfo))
{
return (parameterExpression, expression) => Expression.Call(
methodInfo!,
parameterExpression,
Expression.Constant(CultureInfo.InvariantCulture),
Expression.Constant(DateTimeStyles.None),
expression);
}

if (TryGetNumberStylesTryGetMethod(type, out methodInfo, out var numberStyle))
{
return (parameterExpression, expression) => Expression.Call(
methodInfo!,
parameterExpression,
Expression.Constant(numberStyle),
Expression.Constant(CultureInfo.InvariantCulture),
expression);
}

var staticMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Static)
.OrderByDescending(m => m.GetParameters().Length); ;

foreach (var method in staticMethods)
{
if (method.Name != "TryParse" || method.ReturnType != typeof(bool))
{
continue;
}

var tryParseParameters = method.GetParameters();

if (tryParseParameters.Length == 3 &&
tryParseParameters[0].ParameterType == typeof(string) &&
tryParseParameters[1].ParameterType == typeof(IFormatProvider) &&
tryParseParameters[2].IsOut &&
tryParseParameters[2].ParameterType == type.MakeByRefType())
{
return (parameterExpression, expression) => Expression.Call(
method,
parameterExpression,
Expression.Constant(CultureInfo.InvariantCulture),
expression);
}
else if (tryParseParameters.Length == 2 &&
tryParseParameters[0].ParameterType == typeof(string) &&
tryParseParameters[1].IsOut &&
tryParseParameters[1].ParameterType == type.MakeByRefType())
{
return (parameterExpression, expression) => Expression.Call(method, parameterExpression, expression);
}
}

return null;
}

var underlyingType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
return MethodCallCache.GetOrAdd(underlyingType, Finder);
}

private static MethodInfo GetEnumTryParseMethod()
Expand All @@ -81,5 +180,110 @@ private static MethodInfo GetEnumTryParseMethod()
Debug.Fail("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) not found.");
throw new Exception("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) not found.");
}

private static bool TryGetDateTimeTryPareMethod(Type type, out MethodInfo? methodInfo)
{
methodInfo = null;
if (type != typeof(DateTime) && type != typeof(DateOnly) &&
type != typeof(DateTimeOffset) && type != typeof(TimeOnly))
{
return false;
}

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())
{
methodInfo = method;
break;
}
}

return methodInfo != null;
}

private static bool TryGetNumberStylesTryGetMethod(Type type, out MethodInfo? method, out NumberStyles? numberStyles)
{
method = null;
numberStyles = null;

if (!UseTryParseWithNumberStyleOption(type))
{
return false;
}

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

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

if (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())
{
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))
{
numberStyles = NumberStyles.Integer;
}

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

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

method = methodInfo;
break;
}
}

return true;
}

internal 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);

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