From bad61768c1d05bf042943c01b5c3c0c56c16b49b Mon Sep 17 00:00:00 2001 From: David Fowler <davidfowl@gmail.com> Date: Sun, 19 Jun 2022 08:50:21 -0700 Subject: [PATCH] Fix support for native AOT in minimal (#42272) * Fix support for native AOT in minimal - This includes 2 major changes to fix native AOT support in .NET 7. Explicitly rooting the indexers of various binding sources (query, formfile, header, routes). NativeAOT seems a bit more aggressive here. - We avoid the filter optimization that uses generics to avoid boxing. NativeAOT needs to see generic instantiations statically which we cannot do here. --- .../src/RequestDelegateFactory.cs | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 265351805ef..62c0fd66e66 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -15,6 +15,7 @@ using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -58,6 +59,11 @@ public static partial class RequestDelegateFactory private static readonly MethodInfo PopulateMetadataForEndpointMethod = typeof(RequestDelegateFactory).GetMethod(nameof(PopulateMetadataForEndpoint), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ArrayEmptyOfObjectMethod = typeof(Array).GetMethod(nameof(Array.Empty), BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(new Type[] { typeof(object) }); + private static readonly PropertyInfo QueryIndexerProperty = typeof(IQueryCollection).GetProperty("Item")!; + private static readonly PropertyInfo RouteValuesIndexerProperty = typeof(RouteValueDictionary).GetProperty("Item")!; + private static readonly PropertyInfo HeaderIndexerProperty = typeof(IHeaderDictionary).GetProperty("Item")!; + private static readonly PropertyInfo FormFilesIndexerProperty = typeof(IFormFileCollection).GetProperty("Item")!; + // Call WriteAsJsonAsync<object?>() to serialize the runtime return type rather than the declared return type. // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo<Func<HttpResponse, object?, Task>>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync<object?>(response, value, default)); @@ -98,8 +104,10 @@ public static partial class RequestDelegateFactory private static readonly ConstructorInfo DefaultRouteHandlerInvocationContextConstructor = typeof(DefaultRouteHandlerInvocationContext).GetConstructor(new[] { typeof(HttpContext), typeof(object[]) })!; private static readonly MethodInfo RouteHandlerInvocationContextGetArgument = typeof(RouteHandlerInvocationContext).GetMethod(nameof(RouteHandlerInvocationContext.GetArgument))!; + private static readonly PropertyInfo ListIndexer = typeof(IList<object>).GetProperty("Item")!; private static readonly ParameterExpression FilterContextExpr = Expression.Parameter(typeof(RouteHandlerInvocationContext), "context"); private static readonly MemberExpression FilterContextHttpContextExpr = Expression.Property(FilterContextExpr, typeof(RouteHandlerInvocationContext).GetProperty(nameof(RouteHandlerInvocationContext.HttpContext))!); + private static readonly MemberExpression FilterContextArgumentsExpr = Expression.Property(FilterContextExpr, typeof(RouteHandlerInvocationContext).GetProperty(nameof(RouteHandlerInvocationContext.Arguments))!); private static readonly MemberExpression FilterContextHttpContextResponseExpr = Expression.Property(FilterContextHttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Response))!); private static readonly MemberExpression FilterContextHttpContextStatusCodeExpr = Expression.Property(FilterContextHttpContextResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!); private static readonly ParameterExpression InvokedFilterContextExpr = Expression.Parameter(typeof(RouteHandlerInvocationContext), "filterContext"); @@ -397,6 +405,13 @@ public static partial class RequestDelegateFactory DefaultRouteHandlerInvocationContextConstructor, new Expression[] { HttpContextExpr, paramArray }); + if (!RuntimeFeature.IsDynamicCodeCompiled) + { + // For AOT platforms it's not possible to support the closed generic arguments that are based on the + // parameter arguments dynamically (for value types). In that case, fallback to boxing the argument list. + return fallbackConstruction; + } + var arguments = new Expression[factoryContext.ArgumentExpressions.Length + 1]; arguments[0] = HttpContextExpr; factoryContext.ArgumentExpressions.CopyTo(arguments, 1); @@ -513,16 +528,33 @@ public static partial class RequestDelegateFactory factoryContext.BoxedArgs = new Expression[parameters.Length]; factoryContext.Parameters = new List<ParameterInfo>(parameters); + var hasFilters = factoryContext.Filters is { Count: > 0 }; + for (var i = 0; i < parameters.Length; i++) { args[i] = CreateArgument(parameters[i], factoryContext); - // Register expressions containing the boxed and unboxed variants - // of the route handler's arguments for use in RouteHandlerInvocationContext - // construction and route handler invocation. - // context.GetArgument<string>(0) - factoryContext.ContextArgAccess.Add(Expression.Call(FilterContextExpr, RouteHandlerInvocationContextGetArgument.MakeGenericMethod(parameters[i].ParameterType), Expression.Constant(i))); - // (string, name_local), (int, int_local) + // Only populate the context args if there are filters for this handler + if (hasFilters) + { + if (RuntimeFeature.IsDynamicCodeSupported) + { + // Register expressions containing the boxed and unboxed variants + // of the route handler's arguments for use in RouteHandlerInvocationContext + // construction and route handler invocation. + // context.GetArgument<string>(0) + // (string, name_local), (int, int_local) + factoryContext.ContextArgAccess.Add(Expression.Call(FilterContextExpr, RouteHandlerInvocationContextGetArgument.MakeGenericMethod(parameters[i].ParameterType), Expression.Constant(i))); + } + else + { + // We box if dynamic code isn't supported + factoryContext.ContextArgAccess.Add(Expression.Convert( + Expression.Property(FilterContextArgumentsExpr, ListIndexer, Expression.Constant(i)), + parameters[i].ParameterType)); + } + } + factoryContext.ArgumentTypes[i] = parameters[i].ParameterType; factoryContext.ArgumentExpressions[i] = args[i]; factoryContext.BoxedArgs[i] = Expression.Convert(args[i], typeof(object)); @@ -567,17 +599,17 @@ public static partial class RequestDelegateFactory throw new InvalidOperationException($"'{routeName}' is not a route parameter."); } - return BindParameterFromProperty(parameter, RouteValuesExpr, routeName, factoryContext, "route"); + return BindParameterFromProperty(parameter, RouteValuesExpr, RouteValuesIndexerProperty, routeName, factoryContext, "route"); } else if (parameterCustomAttributes.OfType<IFromQueryMetadata>().FirstOrDefault() is { } queryAttribute) { factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryAttribute); - return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext, "query string"); + return BindParameterFromProperty(parameter, QueryExpr, QueryIndexerProperty, queryAttribute.Name ?? parameter.Name, factoryContext, "query string"); } else if (parameterCustomAttributes.OfType<IFromHeaderMetadata>().FirstOrDefault() is { } headerAttribute) { factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.HeaderAttribute); - return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext, "header"); + return BindParameterFromProperty(parameter, HeadersExpr, HeaderIndexerProperty, headerAttribute.Name ?? parameter.Name, factoryContext, "header"); } else if (parameterCustomAttributes.OfType<IFromBodyMetadata>().FirstOrDefault() is { } bodyAttribute) { @@ -672,12 +704,12 @@ public static partial class RequestDelegateFactory // We're in the fallback case and we have a parameter and route parameter match so don't fallback // to query string in this case factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteParameter); - return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext, "route"); + return BindParameterFromProperty(parameter, RouteValuesExpr, RouteValuesIndexerProperty, parameter.Name, factoryContext, "route"); } else { factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryStringParameter); - return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext, "query string"); + return BindParameterFromProperty(parameter, QueryExpr, QueryIndexerProperty, parameter.Name, factoryContext, "query string"); } } @@ -692,7 +724,7 @@ public static partial class RequestDelegateFactory // We only infer parameter types if you have an array of TryParsables/string[]/StringValues, and DisableInferredFromBody is true factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryStringParameter); - return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext, "query string"); + return BindParameterFromProperty(parameter, QueryExpr, QueryIndexerProperty, parameter.Name, factoryContext, "query string"); } else { @@ -1233,9 +1265,8 @@ public static partial class RequestDelegateFactory } } - private static Expression GetValueFromProperty(Expression sourceExpression, string key, Type? returnType = null) + private static Expression GetValueFromProperty(MemberExpression sourceExpression, PropertyInfo itemProperty, string key, Type? returnType = null) { - var itemProperty = sourceExpression.Type.GetProperty("Item"); var indexArguments = new[] { Expression.Constant(key) }; var indexExpression = Expression.MakeIndex(sourceExpression, itemProperty, indexArguments); return Expression.Convert(indexExpression, returnType ?? typeof(string)); @@ -1601,8 +1632,8 @@ public static partial class RequestDelegateFactory Expression.Convert(Expression.Constant(parameter.DefaultValue), parameter.ParameterType))); } - private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext, string source) => - BindParameterFromValue(parameter, GetValueFromProperty(property, key, GetExpressionType(parameter.ParameterType)), factoryContext, source); + private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, PropertyInfo itemProperty, string key, FactoryContext factoryContext, string source) => + BindParameterFromValue(parameter, GetValueFromProperty(property, itemProperty, key, GetExpressionType(parameter.ParameterType)), factoryContext, source); private static Type? GetExpressionType(Type type) => type.IsArray ? typeof(string[]) : @@ -1611,8 +1642,8 @@ public static partial class RequestDelegateFactory private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, FactoryContext factoryContext) { - var routeValue = GetValueFromProperty(RouteValuesExpr, key); - var queryValue = GetValueFromProperty(QueryExpr, key); + var routeValue = GetValueFromProperty(RouteValuesExpr, RouteValuesIndexerProperty, key); + var queryValue = GetValueFromProperty(QueryExpr, QueryIndexerProperty, key); return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext, "route or query string"); } @@ -1702,7 +1733,7 @@ public static partial class RequestDelegateFactory factoryContext.ReadForm = true; - var valueExpression = GetValueFromProperty(FormFilesExpr, key, typeof(IFormFile)); + var valueExpression = GetValueFromProperty(FormFilesExpr, FormFilesIndexerProperty, key, typeof(IFormFile)); return BindParameterFromExpression(parameter, valueExpression, factoryContext, "form file"); } @@ -2088,7 +2119,7 @@ public static partial class RequestDelegateFactory public Expression? MethodCall { get; set; } public Type[] ArgumentTypes { get; set; } = Array.Empty<Type>(); public Expression[] ArgumentExpressions { get; set; } = Array.Empty<Expression>(); - public Expression[] BoxedArgs { get; set; } = Array.Empty<Expression>(); + public Expression[] BoxedArgs { get; set; } = Array.Empty<Expression>(); public List<Func<RouteHandlerContext, RouteHandlerFilterDelegate, RouteHandlerFilterDelegate>>? Filters { get; init; } public List<ParameterInfo> Parameters { get; set; } = new(); -- GitLab