Skip to content

Commit 83927d9

Browse files
authored
Fix NativeAOT with minimal actions (#35167)
* Fix NativeAOT with minimal actions - Generic methods for value types need to be visible at compile time so that the AOT compiler can generate code for the right instantiations (closed generic types). Since Enum.TryParse<T> is used purely via reflection is gets removed from the final AOT binary. This causes our logic that looks for Enum.TryParse<T> to fail. Instead we call back to the non-generic overload which doesn't get trimmed and can at runtime lookup the metadata for the enum type in order to allow parsing. - Made TryParseMethodCache creatable so it could be tested more easily. - Added tests
1 parent 0a4f4ab commit 83927d9

File tree

4 files changed

+149
-39
lines changed

4 files changed

+149
-39
lines changed

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ namespace Microsoft.AspNetCore.Http
1717
/// </summary>
1818
public static partial class RequestDelegateFactory
1919
{
20-
private static readonly NullabilityInfoContext NullabilityContext = new NullabilityInfoContext();
20+
private static readonly NullabilityInfoContext NullabilityContext = new();
21+
private static readonly TryParseMethodCache TryParseMethodCache = new();
2122

2223
private static readonly MethodInfo ExecuteTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!;
2324
private static readonly MethodInfo ExecuteTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!;
@@ -42,7 +43,6 @@ public static partial class RequestDelegateFactory
4243
private static readonly ParameterExpression HttpContextExpr = Expression.Parameter(typeof(HttpContext), "httpContext");
4344
private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue");
4445
private static readonly ParameterExpression WasParamCheckFailureExpr = Expression.Variable(typeof(bool), "wasParamCheckFailure");
45-
private static readonly ParameterExpression TempSourceStringExpr = TryParseMethodCache.TempSourceStringExpr;
4646

4747
private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.RequestServices));
4848
private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextExpr, nameof(HttpContext.Request));
@@ -55,8 +55,8 @@ public static partial class RequestDelegateFactory
5555
private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, nameof(HttpResponse.StatusCode));
5656
private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo<Func<Task>>(() => Task.CompletedTask));
5757

58-
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null));
59-
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null));
58+
private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TryParseMethodCache.TempSourceStringExpr, Expression.Constant(null));
59+
private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TryParseMethodCache.TempSourceStringExpr, Expression.Constant(null));
6060

6161
/// <summary>
6262
/// Creates a <see cref="RequestDelegate"/> implementation for <paramref name="action"/>.
@@ -168,7 +168,7 @@ public static RequestDelegate Create(MethodInfo methodInfo, Func<HttpContext, ob
168168

169169
if (factoryContext.UsingTempSourceString)
170170
{
171-
responseWritingMethodCall = Expression.Block(new[] { TempSourceStringExpr }, responseWritingMethodCall);
171+
responseWritingMethodCall = Expression.Block(new[] { TryParseMethodCache.TempSourceStringExpr }, responseWritingMethodCall);
172172
}
173173

174174
return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext);
@@ -555,7 +555,7 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
555555
Expression.IfThen(Expression.Equal(argument, Expression.Constant(null)),
556556
Expression.Block(
557557
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
558-
Expression.Call(LogRequiredParameterNotProvidedMethod,
558+
Expression.Call(LogRequiredParameterNotProvidedMethod,
559559
HttpContextExpr, Expression.Constant(parameter.ParameterType.Name), Expression.Constant(parameter.Name))
560560
)
561561
)
@@ -643,7 +643,7 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
643643
var failBlock = Expression.Block(
644644
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
645645
Expression.Call(LogParameterBindingFailureMethod,
646-
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TempSourceStringExpr));
646+
HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, TryParseMethodCache.TempSourceStringExpr));
647647

648648
var tryParseCall = tryParseMethodCall(parsedValue);
649649

@@ -682,14 +682,14 @@ private static Expression BindParameterFromValue(ParameterInfo parameter, Expres
682682
var fullParamCheckBlock = !isOptional
683683
? Expression.Block(
684684
// tempSourceString = httpContext.RequestValue["id"];
685-
Expression.Assign(TempSourceStringExpr, valueExpression),
685+
Expression.Assign(TryParseMethodCache.TempSourceStringExpr, valueExpression),
686686
// if (tempSourceString == null) { ... } only produced when parameter is required
687687
checkRequiredParaseableParameterBlock,
688688
// if (tempSourceString != null) { ... }
689-
ifNotNullTryParse)
689+
ifNotNullTryParse)
690690
: Expression.Block(
691691
// tempSourceString = httpContext.RequestValue["id"];
692-
Expression.Assign(TempSourceStringExpr, valueExpression),
692+
Expression.Assign(TryParseMethodCache.TempSourceStringExpr, valueExpression),
693693
// if (tempSourceString != null) { ... }
694694
ifNotNullTryParse);
695695

@@ -737,7 +737,7 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al
737737
Expression.Equal(BodyValueExpr, Expression.Constant(null)),
738738
Expression.Block(
739739
Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)),
740-
Expression.Call(LogRequiredParameterNotProvidedMethod,
740+
Expression.Call(LogRequiredParameterNotProvidedMethod,
741741
HttpContextExpr, Expression.Constant(parameter.ParameterType.Name), Expression.Constant(parameter.Name))
742742
)
743743
)
@@ -863,8 +863,8 @@ static async Task ExecuteAwaited(Task<string> task, HttpContext httpContext)
863863

864864
private static Task ExecuteWriteStringResponseAsync(HttpContext httpContext, string text)
865865
{
866-
SetPlaintextContentType(httpContext);
867-
return httpContext.Response.WriteAsync(text);
866+
SetPlaintextContentType(httpContext);
867+
return httpContext.Response.WriteAsync(text);
868868
}
869869

870870
private static Task ExecuteValueTask(ValueTask task)

src/Http/Http.Extensions/test/TryParseMethodCacheTests.cs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ public class TryParseMethodCacheTests
2727
[InlineData(typeof(ulong))]
2828
public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCulture(Type @type)
2929
{
30-
var methodFound = TryParseMethodCache.FindTryParseMethod(@type);
30+
var methodFound = new TryParseMethodCache().FindTryParseMethod(@type);
3131

3232
Assert.NotNull(methodFound);
3333

34-
var call = methodFound!(Expression.Variable(type, "parsedValue"));
35-
var parameters = call.Method.GetParameters();
34+
var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression;
35+
Assert.NotNull(call);
36+
var parameters = call!.Method.GetParameters();
3637

3738
Assert.Equal(4, parameters.Length);
3839
Assert.Equal(typeof(string), parameters[0].ParameterType);
@@ -49,12 +50,13 @@ public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCult
4950
[InlineData(typeof(TimeSpan))]
5051
public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCultureDateType(Type @type)
5152
{
52-
var methodFound = TryParseMethodCache.FindTryParseMethod(@type);
53+
var methodFound = new TryParseMethodCache().FindTryParseMethod(@type);
5354

5455
Assert.NotNull(methodFound);
5556

56-
var call = methodFound!(Expression.Variable(type, "parsedValue"));
57-
var parameters = call.Method.GetParameters();
57+
var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression;
58+
Assert.NotNull(call);
59+
var parameters = call!.Method.GetParameters();
5860

5961
if (@type == typeof(TimeSpan))
6062
{
@@ -77,12 +79,13 @@ public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCult
7779
[InlineData(typeof(TryParsableInvariantRecord))]
7880
public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCultureCustomType(Type @type)
7981
{
80-
var methodFound = TryParseMethodCache.FindTryParseMethod(@type);
82+
var methodFound = new TryParseMethodCache().FindTryParseMethod(@type);
8183

8284
Assert.NotNull(methodFound);
8385

84-
var call = methodFound!(Expression.Variable(type, "parsedValue"));
85-
var parameters = call.Method.GetParameters();
86+
var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression;
87+
Assert.NotNull(call);
88+
var parameters = call!.Method.GetParameters();
8689

8790
Assert.Equal(3, parameters.Length);
8891
Assert.Equal(typeof(string), parameters[0].ParameterType);
@@ -91,6 +94,56 @@ public void FindTryParseMethod_ReturnsTheExpectedTryParseMethodWithInvariantCult
9194
Assert.True(((call.Arguments[1] as ConstantExpression)!.Value as CultureInfo)!.Equals(CultureInfo.InvariantCulture));
9295
}
9396

97+
[Fact]
98+
public void FindTryParseMethodForEnums()
99+
{
100+
var type = typeof(Choice);
101+
var methodFound = new TryParseMethodCache().FindTryParseMethod(type);
102+
103+
Assert.NotNull(methodFound);
104+
105+
var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression;
106+
Assert.NotNull(call);
107+
var method = call!.Method;
108+
var parameters = method.GetParameters();
109+
110+
// By default, we use Enum.TryParse<T>
111+
Assert.True(method.IsGenericMethod);
112+
Assert.Equal(2, parameters.Length);
113+
Assert.Equal(typeof(string), parameters[0].ParameterType);
114+
Assert.True(parameters[1].IsOut);
115+
}
116+
117+
[Fact]
118+
public void FindTryParseMethodForEnumsWhenNonGenericEnumParseIsUsed()
119+
{
120+
var type = typeof(Choice);
121+
var cache = new TryParseMethodCache(preferNonGenericEnumParseOverload: true);
122+
var methodFound = cache.FindTryParseMethod(type);
123+
124+
Assert.NotNull(methodFound);
125+
126+
var parsedValue = Expression.Variable(type, "parsedValue");
127+
var block = methodFound!(parsedValue) as BlockExpression;
128+
Assert.NotNull(block);
129+
Assert.Equal(typeof(bool), block!.Type);
130+
131+
var parseEnum = Expression.Lambda<Func<string, Choice>>(Expression.Block(new[] { parsedValue },
132+
block,
133+
parsedValue), cache.TempSourceStringExpr).Compile();
134+
135+
Assert.Equal(Choice.One, parseEnum("One"));
136+
Assert.Equal(Choice.Two, parseEnum("Two"));
137+
Assert.Equal(Choice.Three, parseEnum("Three"));
138+
}
139+
140+
enum Choice
141+
{
142+
One,
143+
Two,
144+
Three
145+
}
146+
94147
private record TryParsableInvariantRecord(int value)
95148
{
96149
public static bool TryParse(string? value, IFormatProvider formatProvider, out TryParsableInvariantRecord? result)

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ internal class EndpointMetadataApiDescriptionProvider : IApiDescriptionProvider
2727
private readonly EndpointDataSource _endpointDataSource;
2828
private readonly IHostEnvironment _environment;
2929
private readonly IServiceProviderIsService? _serviceProviderIsService;
30+
private readonly TryParseMethodCache TryParseMethodCache = new();
3031

3132
// Executes before MVC's DefaultApiDescriptionProvider and GrpcHttpApiDescriptionProvider for no particular reason.
3233
public int Order => -1100;

src/Shared/TryParseMethodCache.cs

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,64 @@
1515

1616
namespace Microsoft.AspNetCore.Http
1717
{
18-
internal static class TryParseMethodCache
18+
internal sealed class TryParseMethodCache
1919
{
20-
private static readonly MethodInfo EnumTryParseMethod = GetEnumTryParseMethod();
20+
private readonly MethodInfo _enumTryParseMethod;
2121

2222
// Since this is shared source, the cache won't be shared between RequestDelegateFactory and the ApiDescriptionProvider sadly :(
23-
private static readonly ConcurrentDictionary<Type, Func<Expression, MethodCallExpression>?> MethodCallCache = new();
24-
internal static readonly ParameterExpression TempSourceStringExpr = Expression.Variable(typeof(string), "tempSourceString");
23+
private readonly ConcurrentDictionary<Type, Func<Expression, Expression>?> _methodCallCache = new();
2524

26-
public static bool HasTryParseMethod(ParameterInfo parameter)
25+
internal readonly ParameterExpression TempSourceStringExpr = Expression.Variable(typeof(string), "tempSourceString");
26+
27+
public TryParseMethodCache() : this(preferNonGenericEnumParseOverload: false)
28+
{
29+
}
30+
31+
// This is for testing
32+
public TryParseMethodCache(bool preferNonGenericEnumParseOverload)
33+
{
34+
_enumTryParseMethod = GetEnumTryParseMethod(preferNonGenericEnumParseOverload);
35+
}
36+
37+
public bool HasTryParseMethod(ParameterInfo parameter)
2738
{
2839
var nonNullableParameterType = Nullable.GetUnderlyingType(parameter.ParameterType) ?? parameter.ParameterType;
2940
return FindTryParseMethod(nonNullableParameterType) is not null;
3041
}
3142

32-
public static Func<Expression, MethodCallExpression>? FindTryParseMethod(Type type)
43+
public Func<Expression, Expression>? FindTryParseMethod(Type type)
3344
{
34-
static Func<Expression, MethodCallExpression>? Finder(Type type)
45+
Func<Expression, Expression>? Finder(Type type)
3546
{
3647
MethodInfo? methodInfo;
3748

3849
if (type.IsEnum)
3950
{
40-
methodInfo = EnumTryParseMethod.MakeGenericMethod(type);
41-
if (methodInfo != null)
51+
if (_enumTryParseMethod.IsGenericMethod)
4252
{
53+
methodInfo = _enumTryParseMethod.MakeGenericMethod(type);
54+
4355
return (expression) => Expression.Call(methodInfo!, TempSourceStringExpr, expression);
4456
}
4557

46-
return null;
58+
return (expression) =>
59+
{
60+
var enumAsObject = Expression.Variable(typeof(object), "enumAsObject");
61+
var success = Expression.Variable(typeof(bool), "success");
62+
63+
// object enumAsObject;
64+
// bool success;
65+
// success = Enum.TryParse(type, tempSourceString, out enumAsObject);
66+
// parsedValue = success ? (Type)enumAsObject : default;
67+
// return success;
68+
69+
return Expression.Block(new[] { success, enumAsObject },
70+
Expression.Assign(success, Expression.Call(_enumTryParseMethod, Expression.Constant(type), TempSourceStringExpr, enumAsObject)),
71+
Expression.Assign(expression,
72+
Expression.Condition(success, Expression.Convert(enumAsObject, type), Expression.Default(type))),
73+
success);
74+
};
75+
4776
}
4877

4978
if (TryGetDateTimeTryParseMethod(type, out methodInfo))
@@ -87,32 +116,59 @@ public static bool HasTryParseMethod(ParameterInfo parameter)
87116
return null;
88117
}
89118

90-
return MethodCallCache.GetOrAdd(type, Finder);
119+
return _methodCallCache.GetOrAdd(type, Finder);
91120
}
92121

93-
private static MethodInfo GetEnumTryParseMethod()
122+
private static MethodInfo GetEnumTryParseMethod(bool preferNonGenericEnumParseOverload)
94123
{
95124
var staticEnumMethods = typeof(Enum).GetMethods(BindingFlags.Public | BindingFlags.Static);
96125

126+
// With NativeAOT, if there's no static usage of Enum.TryParse<T>, it will be removed
127+
// we fallback to the non-generic version if that is the case
128+
MethodInfo? genericCandidate = null;
129+
MethodInfo? nonGenericCandidate = null;
130+
97131
foreach (var method in staticEnumMethods)
98132
{
99-
if (!method.IsGenericMethod || method.Name != nameof(Enum.TryParse) || method.ReturnType != typeof(bool))
133+
if (method.Name != nameof(Enum.TryParse) || method.ReturnType != typeof(bool))
100134
{
101135
continue;
102136
}
103137

104138
var tryParseParameters = method.GetParameters();
105139

106-
if (tryParseParameters.Length == 2 &&
140+
// Enum.TryParse<T>(string, out object)
141+
if (method.IsGenericMethod &&
142+
tryParseParameters.Length == 2 &&
107143
tryParseParameters[0].ParameterType == typeof(string) &&
108144
tryParseParameters[1].IsOut)
109145
{
110-
return method;
146+
genericCandidate = method;
111147
}
148+
149+
// Enum.TryParse(type, string, out object)
150+
if (!method.IsGenericMethod &&
151+
tryParseParameters.Length == 3 &&
152+
tryParseParameters[0].ParameterType == typeof(Type) &&
153+
tryParseParameters[1].ParameterType == typeof(string) &&
154+
tryParseParameters[2].IsOut)
155+
{
156+
nonGenericCandidate = method;
157+
}
158+
}
159+
160+
if (genericCandidate is null && nonGenericCandidate is null)
161+
{
162+
Debug.Fail("No suitable System.Enum.TryParse method found.");
163+
throw new MissingMethodException("No suitable System.Enum.TryParse method found.");
164+
}
165+
166+
if (preferNonGenericEnumParseOverload)
167+
{
168+
return nonGenericCandidate!;
112169
}
113170

114-
Debug.Fail("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) not found.");
115-
throw new Exception("static bool System.Enum.TryParse<TEnum>(string? value, out TEnum result) not found.");
171+
return genericCandidate ?? nonGenericCandidate!;
116172
}
117173

118174
private static bool TryGetDateTimeTryParseMethod(Type type, [NotNullWhen(true)] out MethodInfo? methodInfo)

0 commit comments

Comments
 (0)