Skip to content

Commit

Permalink
Surrogate argument list in Minimal APIs (#41325)
Browse files Browse the repository at this point in the history
* Core funcionality

* Changing the order

* Not checking attribute from type

* Code cleanup

* Adding missing ParamCheckExpression

* Adding support for records and structs

* change to a static local function

* Remove empty line

* Updating APIExplorer

* Updating APIExplorer

* Updating OpenAPI

* PR feedback

* Updating comment

* Allowing attribute on classes

* Reducing initial memory allocation

* Adding constructorinfo caching

* Updating OpenAPI generator

* Updating APIExplorer

* Adding constructor cache

* Renaming to SurrogateParameterInfo

* Updating OpenAPI Generator

* Updating ApiExplorer

* Updating RequestDelegateFactory

* Adding initial test cases

* Rollback bad change

* Fixing merge issues

* Initial SurrogateParameterInfo tests

* Adding surrogateparameterinfo tests

* Using Span

* Adding FindConstructor unit tests

* Using span

* Updating error message

* Updating surrogateParameteInfo and fix unit test

* Adding RequestDelegateFactory tests

* Code cleanup

* code clean up

* code clean up

* Adding suppress

* Adding trimming warning suppress

* PR feeback

* PR feeback

* Mark types as sealed

* Seal SurrogateParameterInfo

* Removing attribute from type

* API Review changes

* Updating documentation

* Renaming surrogateParameterInfo

* Code cleanup

* Code cleanup

* Code cleanup

* Code cleanup

* Updating tests to include FromService in properties

* Renaming to BindPropertiesAsParameter

* Renaming to BindParameterFromProperties

* Adding more FromServices tests

* PR Feedback

* PR Feeback

* Merging with latest OpenAPI changes

* adding more tests

* Update src/Shared/ParameterBindingMethodCache.cs

Co-authored-by: Brennan <brecon@microsoft.com>

* Updating errormessag on unit tests

* Updating errormessag on unit tests

* PR Feedback

Co-authored-by: Brennan <brecon@microsoft.com>
  • Loading branch information
brunolins16 and BrennanConroy authored May 12, 2022
1 parent c014e81 commit 65bb1ec
Show file tree
Hide file tree
Showing 23 changed files with 1,839 additions and 31 deletions.
17 changes: 17 additions & 0 deletions src/Http/Http.Abstractions/src/AsParametersAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http;

using System;

/// <summary>
/// Specifies that a route handler delegate's parameter represents a structured parameter list.
/// </summary>
[AttributeUsage(
AttributeTargets.Parameter,
Inherited = false,
AllowMultiple = false)]
public sealed class AsParametersAttribute : Attribute
{
}
2 changes: 2 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*REMOVED*Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator.Current.get -> object?
Microsoft.AspNetCore.Builder.EndpointBuilder.ServiceProvider.get -> System.IServiceProvider?
Microsoft.AspNetCore.Builder.EndpointBuilder.ServiceProvider.set -> void
Microsoft.AspNetCore.Http.AsParametersAttribute
Microsoft.AspNetCore.Http.AsParametersAttribute.AsParametersAttribute() -> void
Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext
Microsoft.AspNetCore.Http.DefaultRouteHandlerInvocationContext.DefaultRouteHandlerInvocationContext(Microsoft.AspNetCore.Http.HttpContext! httpContext, params object![]! arguments) -> void
Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator.Current.get -> object!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>ASP.NET Core common extension methods for HTTP abstractions, HTTP headers, HTTP request/response, and session state.</Description>
Expand All @@ -13,6 +13,7 @@
<ItemGroup>
<Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\**\*.cs" LinkBase="Shared"/>
<Compile Include="$(SharedSourceRoot)ParameterBindingMethodCache.cs" LinkBase="Shared"/>
<Compile Include="$(SharedSourceRoot)PropertyAsParameterInfo.cs" LinkBase="Shared"/>
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)ProblemDetailsJsonConverter.cs" LinkBase="Shared"/>
<Compile Include="$(SharedSourceRoot)HttpValidationProblemDetailsJsonConverter.cs" LinkBase="Shared" />
Expand Down
97 changes: 91 additions & 6 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -222,7 +223,10 @@ private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions
factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, arguments);

// Add metadata provided by the delegate return type and parameter types next, this will be more specific than inferred metadata from above
AddTypeProvidedMetadata(methodInfo, factoryContext.Metadata, factoryContext.ServiceProvider);
AddTypeProvidedMetadata(methodInfo,
factoryContext.Metadata,
factoryContext.ServiceProvider,
CollectionsMarshal.AsSpan(factoryContext.Parameters));

// Add method attributes as metadata *after* any inferred metadata so that the attributes hava a higher specificity
AddMethodAttributesAsMetadata(methodInfo, factoryContext.Metadata);
Expand Down Expand Up @@ -424,12 +428,11 @@ private static Expression CreateRouteHandlerInvocationContextBase(FactoryContext
return fallbackConstruction;
}

private static void AddTypeProvidedMetadata(MethodInfo methodInfo, List<object> metadata, IServiceProvider? services)
private static void AddTypeProvidedMetadata(MethodInfo methodInfo, List<object> metadata, IServiceProvider? services, ReadOnlySpan<ParameterInfo> parameters)
{
object?[]? invokeArgs = null;

// Get metadata from parameter types
var parameters = methodInfo.GetParameters();
foreach (var parameter in parameters)
{
if (typeof(IEndpointParameterMetadataProvider).IsAssignableFrom(parameter.ParameterType))
Expand Down Expand Up @@ -503,10 +506,12 @@ private static Expression[] CreateArguments(ParameterInfo[]? parameters, Factory
factoryContext.ArgumentTypes = new Type[parameters.Length];
factoryContext.ArgumentExpressions = new Expression[parameters.Length];
factoryContext.BoxedArgs = new Expression[parameters.Length];
factoryContext.Parameters = new List<ParameterInfo>(parameters);

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.
Expand Down Expand Up @@ -599,6 +604,16 @@ private static Expression CreateArgument(ParameterInfo parameter, FactoryContext
factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceAttribute);
return BindParameterFromService(parameter, factoryContext);
}
else if (parameterCustomAttributes.OfType<AsParametersAttribute>().Any())
{
if (parameter is PropertyAsParameterInfo)
{
throw new NotSupportedException(
$"Nested {nameof(AsParametersAttribute)} is not supported and should be used only for handler parameters.");
}

return BindParameterFromProperties(parameter, factoryContext);
}
else if (parameter.ParameterType == typeof(HttpContext))
{
return HttpContextExpr;
Expand Down Expand Up @@ -1221,6 +1236,68 @@ private static Expression GetValueFromProperty(Expression sourceExpression, stri
return Expression.Convert(indexExpression, returnType ?? typeof(string));
}

private static Expression BindParameterFromProperties(ParameterInfo parameter, FactoryContext factoryContext)
{
var argumentExpression = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");
var (constructor, parameters) = ParameterBindingMethodCache.FindConstructor(parameter.ParameterType);

if (constructor is not null && parameters is { Length: > 0 })
{
// arg_local = new T(....)

var constructorArguments = new Expression[parameters.Length];

for (var i = 0; i < parameters.Length; i++)
{
var parameterInfo =
new PropertyAsParameterInfo(parameters[i].PropertyInfo, parameters[i].ParameterInfo, factoryContext.NullabilityContext);
constructorArguments[i] = CreateArgument(parameterInfo, factoryContext);
factoryContext.Parameters.Add(parameterInfo);
}

factoryContext.ParamCheckExpressions.Add(
Expression.Assign(
argumentExpression,
Expression.New(constructor, constructorArguments)));
}
else
{
// arg_local = new T()
// {
// arg_local.Property[0] = expression[0],
// arg_local.Property[n] = expression[n],
// }

var properties = parameter.ParameterType.GetProperties();
var bindings = new List<MemberBinding>(properties.Length);

for (var i = 0; i < properties.Length; i++)
{
// For parameterless ctor we will init only writable properties.
if (properties[i].CanWrite)
{
var parameterInfo = new PropertyAsParameterInfo(properties[i], factoryContext.NullabilityContext);
bindings.Add(Expression.Bind(properties[i], CreateArgument(parameterInfo, factoryContext)));
factoryContext.Parameters.Add(parameterInfo);
}
}

var newExpression = constructor is null ?
Expression.New(parameter.ParameterType) :
Expression.New(constructor);

factoryContext.ParamCheckExpressions.Add(
Expression.Assign(
argumentExpression,
Expression.MemberInit(newExpression, bindings)));
}

factoryContext.TrackedParameters.Add(parameter.Name!, RequestDelegateFactoryConstants.PropertyAsParameter);
factoryContext.ExtraLocals.Add(argumentExpression);

return argumentExpression;
}

private static Expression BindParameterFromService(ParameterInfo parameter, FactoryContext factoryContext)
{
var isOptional = IsOptionalParameter(parameter, factoryContext);
Expand Down Expand Up @@ -1711,15 +1788,20 @@ private static Expression BindParameterFromBody(ParameterInfo parameter, bool al

private static bool IsOptionalParameter(ParameterInfo parameter, FactoryContext factoryContext)
{
if (parameter is PropertyAsParameterInfo argument)
{
return argument.IsOptional;
}

// - Parameters representing value or reference types with a default value
// under any nullability context are treated as optional.
// - Value type parameters without a default value in an oblivious
// nullability context are required.
// - Reference type parameters without a default value in an oblivious
// nullability context are optional.
var nullability = factoryContext.NullabilityContext.Create(parameter);
var nullabilityInfo = factoryContext.NullabilityContext.Create(parameter);
return parameter.HasDefaultValue
|| nullability.ReadState != NullabilityState.NotNull;
|| nullabilityInfo.ReadState != NullabilityState.NotNull;
}

private static MethodInfo GetMethodInfo<T>(Expression<T> expr)
Expand Down Expand Up @@ -2000,9 +2082,11 @@ private sealed class FactoryContext
public List<Expression> ContextArgAccess { get; } = new();
public Expression? MethodCall { get; set; }
public Type[] ArgumentTypes { get; set; } = Array.Empty<Type>();
public Expression[] ArgumentExpressions { get; set; } = Array.Empty<Expression>();
public Expression[] ArgumentExpressions { 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();
}

private static class RequestDelegateFactoryConstants
Expand All @@ -2019,6 +2103,7 @@ private static class RequestDelegateFactoryConstants
public const string BodyParameter = "Body (Inferred)";
public const string RouteOrQueryStringParameter = "Route or Query String (Inferred)";
public const string FormFileParameter = "Form File (Inferred)";
public const string PropertyAsParameter = "As Parameter (Attribute)";
}

private static partial class Log
Expand Down
Loading

0 comments on commit 65bb1ec

Please sign in to comment.