Browse Source

Surrogate argument list in Minimal APIs (#41325)

* 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 <[email protected]>

* Updating errormessag on unit tests

* Updating errormessag on unit tests

* PR Feedback

Co-authored-by: Brennan <[email protected]>
Bruno Oliveira 3 years ago
parent
commit
65bb1ec1e3
23 changed files with 1839 additions and 31 deletions
  1. 17 0
      src/Http/Http.Abstractions/src/AsParametersAttribute.cs
  2. 2 0
      src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
  3. 2 1
      src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj
  4. 91 6
      src/Http/Http.Extensions/src/RequestDelegateFactory.cs
  5. 278 0
      src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs
  6. 224 0
      src/Http/Http.Extensions/test/PropertyAsParameterInfoTests.cs
  7. 665 4
      src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs
  8. 7 9
      src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs
  9. 1 0
      src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj
  10. 6 0
      src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs
  11. 80 0
      src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs
  12. 2 2
      src/Mvc/Mvc.Core/src/FromServicesAttribute.cs
  13. 26 2
      src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs
  14. 15 1
      src/Mvc/Mvc.RazorPages/test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs
  15. 17 1
      src/Mvc/Mvc.RazorPages/test/Infrastructure/PageBinderFactoryTest.cs
  16. 1 0
      src/Mvc/test/Mvc.FunctionalTests/RequestServicesTestBase.cs
  17. 2 2
      src/Mvc/test/Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs
  18. 9 0
      src/Mvc/test/WebSites/BasicWebSite/Controllers/RequestScopedServiceController.cs
  19. 1 0
      src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj
  20. 3 2
      src/OpenApi/src/OpenApiGenerator.cs
  21. 79 1
      src/OpenApi/test/OpenApiGeneratorTests.cs
  22. 134 0
      src/Shared/ParameterBindingMethodCache.cs
  23. 177 0
      src/Shared/PropertyAsParameterInfo.cs

+ 17 - 0
src/Http/Http.Abstractions/src/AsParametersAttribute.cs

@@ -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 - 0
src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

@@ -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!

+ 2 - 1
src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj

@@ -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>
@@ -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" />

+ 91 - 6
src/Http/Http.Extensions/src/RequestDelegateFactory.cs

@@ -9,6 +9,7 @@ using System.Linq;
 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;
@@ -222,7 +223,10 @@ public static partial class RequestDelegateFactory
         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);
@@ -424,12 +428,11 @@ public static partial class RequestDelegateFactory
         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))
@@ -503,10 +506,12 @@ public static partial class RequestDelegateFactory
         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.
@@ -599,6 +604,16 @@ public static partial class RequestDelegateFactory
             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;
@@ -1221,6 +1236,68 @@ public static partial class RequestDelegateFactory
         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);
@@ -1711,15 +1788,20 @@ public static partial class RequestDelegateFactory
 
     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)
@@ -2000,9 +2082,11 @@ public static partial class RequestDelegateFactory
         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
@@ -2019,6 +2103,7 @@ public static partial class RequestDelegateFactory
         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

+ 278 - 0
src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs

@@ -359,6 +359,70 @@ public class ParameterBindingMethodCacheTests
         Assert.Null(await parseHttpContext(httpContext));
     }
 
+    [Theory]
+    [InlineData(typeof(ClassWithParameterlessConstructor))]
+    [InlineData(typeof(RecordClassParameterlessConstructor))]
+    [InlineData(typeof(StructWithParameterlessConstructor))]
+    [InlineData(typeof(RecordStructWithParameterlessConstructor))]
+    public void FindConstructor_FindsParameterlessConstructor_WhenExplicitlyDeclared(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var (constructor, parameters) = cache.FindConstructor(type);
+
+        Assert.NotNull(constructor);
+        Assert.True(parameters.Length == 0);
+    }
+
+    [Theory]
+    [InlineData(typeof(ClassWithDefaultConstructor))]
+    [InlineData(typeof(RecordClassWithDefaultConstructor))]
+    public void FindConstructor_FindsDefaultConstructor_WhenNotExplictlyDeclared(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var (constructor, parameters) = cache.FindConstructor(type);
+
+        Assert.NotNull(constructor);
+        Assert.True(parameters.Length == 0);
+    }
+
+    [Theory]
+    [InlineData(typeof(ClassWithParameterizedConstructor))]
+    [InlineData(typeof(RecordClassParameterizedConstructor))]
+    [InlineData(typeof(StructWithParameterizedConstructor))]
+    [InlineData(typeof(RecordStructParameterizedConstructor))]
+    public void FindConstructor_FindsParameterizedConstructor_WhenExplictlyDeclared(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var (constructor, parameters) = cache.FindConstructor(type);
+
+        Assert.NotNull(constructor);
+        Assert.True(parameters.Length == 1);
+    }
+
+    [Theory]
+    [InlineData(typeof(StructWithDefaultConstructor))]
+    [InlineData(typeof(RecordStructWithDefaultConstructor))]
+    public void FindConstructor_ReturnNullForStruct_WhenNotExplictlyDeclared(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var (constructor, parameters) = cache.FindConstructor(type);
+
+        Assert.Null(constructor);
+        Assert.True(parameters.Length == 0);
+    }
+
+    [Theory]
+    [InlineData(typeof(StructWithMultipleConstructors))]
+    [InlineData(typeof(RecordStructWithMultipleConstructors))]
+    public void FindConstructor_ReturnNullForStruct_WhenMultipleParameterizedConstructorsDeclared(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var (constructor, parameters) = cache.FindConstructor(type);
+
+        Assert.Null(constructor);
+        Assert.True(parameters.Length == 0);
+    }
+
     public static TheoryData<Type> InvalidTryParseStringTypesData
     {
         get
@@ -493,6 +557,61 @@ public class ParameterBindingMethodCacheTests
         Assert.NotNull(expression);
     }
 
+    private class ClassWithInternalConstructor
+    {
+        internal ClassWithInternalConstructor()
+        { }
+    }
+    private record RecordWithInternalConstructor
+    {
+        internal RecordWithInternalConstructor()
+        { }
+    }
+
+    [Theory]
+    [InlineData(typeof(ClassWithInternalConstructor))]
+    [InlineData(typeof(RecordWithInternalConstructor))]
+    public void FindConstructor_ThrowsIfNoPublicConstructors(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var ex = Assert.Throws<InvalidOperationException>(() => cache.FindConstructor(type));
+        Assert.Equal($"No public parameterless constructor found for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.", ex.Message);
+    }
+
+    [Theory]
+    [InlineData(typeof(AbstractClass))]
+    [InlineData(typeof(AbstractRecord))]
+    public void FindConstructor_ThrowsIfAbstract(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var ex = Assert.Throws<InvalidOperationException>(() => cache.FindConstructor(type));
+        Assert.Equal($"The abstract type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}' is not supported.", ex.Message);
+    }
+
+    [Theory]
+    [InlineData(typeof(ClassWithMultipleConstructors))]
+    [InlineData(typeof(RecordWithMultipleConstructors))]
+    public void FindConstructor_ThrowsIfMultipleParameterizedConstructors(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var ex = Assert.Throws<InvalidOperationException>(() => cache.FindConstructor(type));
+        Assert.Equal($"Only a single public parameterized constructor is allowed for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.", ex.Message);
+    }
+
+    [Theory]
+    [InlineData(typeof(ClassWithInvalidConstructors))]
+    [InlineData(typeof(RecordClassWithInvalidConstructors))]
+    [InlineData(typeof(RecordStructWithInvalidConstructors))]
+    [InlineData(typeof(StructWithInvalidConstructors))]
+    public void FindConstructor_ThrowsIfParameterizedConstructorIncludeNoMatchingArguments(Type type)
+    {
+        var cache = new ParameterBindingMethodCache();
+        var ex = Assert.Throws<InvalidOperationException>(() => cache.FindConstructor(type));
+        Assert.Equal(
+            $"The public parameterized constructor must contain only parameters that match the declared public properties for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.",
+            ex.Message);
+    }
+
     enum Choice
     {
         One,
@@ -1069,6 +1188,165 @@ public class ParameterBindingMethodCacheTests
             => throw new NotImplementedException();
     }
 
+    public class ClassWithParameterizedConstructor
+    {
+        public int Foo { get; set; }
+
+        public ClassWithParameterizedConstructor(int foo)
+        {
+
+        }
+    }
+
+    public record RecordClassParameterizedConstructor(int Foo);
+
+    public record struct RecordStructParameterizedConstructor(int Foo);
+
+    public struct StructWithParameterizedConstructor
+    {
+        public int Foo { get; set; }
+
+        public StructWithParameterizedConstructor(int foo)
+        {
+            Foo = foo;
+        }
+    }
+
+    public class ClassWithParameterlessConstructor
+    {
+        public ClassWithParameterlessConstructor()
+        {
+        }
+
+        public ClassWithParameterlessConstructor(int foo)
+        {
+
+        }
+    }
+
+    public record RecordClassParameterlessConstructor
+    {
+        public RecordClassParameterlessConstructor()
+        {
+        }
+
+        public RecordClassParameterlessConstructor(int foo)
+        {
+
+        }
+    }
+
+    public struct StructWithParameterlessConstructor
+    {
+        public StructWithParameterlessConstructor()
+        {
+        }
+
+        public StructWithParameterlessConstructor(int foo)
+        {
+        }
+    }
+
+    public record struct RecordStructWithParameterlessConstructor
+    {
+        public RecordStructWithParameterlessConstructor()
+        {
+        }
+
+        public RecordStructWithParameterlessConstructor(int foo)
+        {
+
+        }
+    }
+
+    public class ClassWithDefaultConstructor
+    { }
+    public record RecordClassWithDefaultConstructor
+    { }
+
+    public struct StructWithDefaultConstructor
+    { }
+
+    public record struct RecordStructWithDefaultConstructor
+    { }
+
+    public struct StructWithMultipleConstructors
+    {
+        public StructWithMultipleConstructors(int foo)
+        {
+        }
+        public StructWithMultipleConstructors(int foo, int bar)
+        {
+        }
+    }
+
+    public record struct RecordStructWithMultipleConstructors(int Foo)
+    {
+        public RecordStructWithMultipleConstructors(int foo, int bar)
+            : this(foo)
+        {
+
+        }
+    }
+
+    private abstract class AbstractClass { }
+
+    private abstract record AbstractRecord();
+
+    private class ClassWithMultipleConstructors
+    {
+        public ClassWithMultipleConstructors(int foo)
+        { }
+
+        public ClassWithMultipleConstructors(int foo, int bar)
+        { }
+    }
+
+    private record RecordWithMultipleConstructors
+    {
+        public RecordWithMultipleConstructors(int foo)
+        { }
+
+        public RecordWithMultipleConstructors(int foo, int bar)
+        { }
+    }
+
+    private class ClassWithInvalidConstructors
+    {
+        public int Foo { get; set; }
+
+        public ClassWithInvalidConstructors(int foo, int bar)
+        { }
+    }
+
+    private record RecordClassWithInvalidConstructors
+    {
+        public int Foo { get; set; }
+
+        public RecordClassWithInvalidConstructors(int foo, int bar)
+        { }
+    }
+
+    private struct StructWithInvalidConstructors
+    {
+        public int Foo { get; set; }
+
+        public StructWithInvalidConstructors(int foo, int bar)
+        {
+            Foo = foo;
+        }
+    }
+
+    private record struct RecordStructWithInvalidConstructors
+    {
+        public int Foo { get; set; }
+
+        public RecordStructWithInvalidConstructors(int foo, int bar)
+        {
+            Foo = foo;
+        }
+    }
+
     private class MockParameterInfo : ParameterInfo
     {
         public MockParameterInfo(Type type, string name)

+ 224 - 0
src/Http/Http.Extensions/test/PropertyAsParameterInfoTests.cs

@@ -0,0 +1,224 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Reflection;
+
+namespace Microsoft.AspNetCore.Http.Extensions.Tests;
+
+public class PropertyAsParameterInfoTests
+{
+    [Fact]
+    public void Initialization_SetsTypeAndNameFromPropertyInfo()
+    {
+        // Arrange & Act
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.NoAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo);
+
+        // Assert
+        Assert.Equal(propertyInfo.Name, parameterInfo.Name);
+        Assert.Equal(propertyInfo.PropertyType, parameterInfo.ParameterType);
+    }
+
+    [Fact]
+    public void Initialization_WithConstructorArgument_SetsTypeAndNameFromPropertyInfo()
+    {
+        // Arrange & Act
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.NoAttribute));
+        var parameter = GetParameter(nameof(ArgumentList.DefaultMethod), nameof(ArgumentList.NoAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo, parameter);
+
+        // Assert
+        Assert.Equal(propertyInfo.Name, parameterInfo.Name);
+        Assert.Equal(propertyInfo.PropertyType, parameterInfo.ParameterType);
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_ContainsPropertyCustomAttributes()
+    {
+        // Arrange
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.WithTestAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo);
+
+        // Act & Assert
+        Assert.Single(parameterInfo.GetCustomAttributes(typeof(TestAttribute)));
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_WithConstructorArgument_UsesParameterCustomAttributes()
+    {
+        // Arrange
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.NoAttribute));
+        var parameter = GetParameter(nameof(ArgumentList.DefaultMethod), nameof(ArgumentList.WithTestAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo, parameter);
+
+        // Act & Assert
+        Assert.Single(parameterInfo.GetCustomAttributes(typeof(TestAttribute)));
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_WithConstructorArgument_FallbackToPropertyCustomAttributes()
+    {
+        // Arrange
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.WithTestAttribute));
+        var parameter = GetParameter(nameof(ArgumentList.DefaultMethod), nameof(ArgumentList.NoAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo, parameter);
+
+        // Act & Assert
+        Assert.Single(parameterInfo.GetCustomAttributes(typeof(TestAttribute)));
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_ContainsPropertyCustomAttributesData()
+    {
+        // Arrange
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.WithTestAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo);
+
+        // Act
+        var attributes = parameterInfo.GetCustomAttributesData();
+
+        // Assert
+        Assert.Single(
+            attributes,
+            a => typeof(TestAttribute).IsAssignableFrom(a.AttributeType));
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_WithConstructorArgument_MergePropertyAndParameterCustomAttributesData()
+    {
+        // Arrange & Act
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.WithTestAttribute));
+        var parameter = GetParameter(nameof(ArgumentList.DefaultMethod), nameof(ArgumentList.WithSampleAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo, parameter);
+
+        // Act
+        var attributes = parameterInfo.GetCustomAttributesData();
+
+        // Assert
+        Assert.Single(
+            parameterInfo.GetCustomAttributesData(),
+            a => typeof(TestAttribute).IsAssignableFrom(a.AttributeType));
+        Assert.Single(
+            parameterInfo.GetCustomAttributesData(),
+            a => typeof(SampleAttribute).IsAssignableFrom(a.AttributeType));
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_WithConstructorArgument_MergePropertyAndParameterCustomAttributes()
+    {
+        // Arrange
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.WithTestAttribute));
+        var parameter = GetParameter(nameof(ArgumentList.DefaultMethod), nameof(ArgumentList.WithSampleAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo, parameter);
+
+        // Act
+        var attributes = parameterInfo.GetCustomAttributes(true);
+
+        // Assert
+        Assert.Single(
+            attributes,
+            a => typeof(TestAttribute).IsAssignableFrom(a.GetType()));
+        Assert.Single(
+            attributes,
+            a => typeof(SampleAttribute).IsAssignableFrom(a.GetType()));
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_ContainsPropertyInheritedCustomAttributes()
+    {
+        // Arrange & Act
+        var propertyInfo = GetProperty(typeof(DerivedArgumentList), nameof(DerivedArgumentList.WithTestAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo);
+
+        // Assert
+        Assert.Single(parameterInfo.GetCustomAttributes(typeof(TestAttribute), true));
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_DoesNotHaveDefaultValueFromProperty()
+    {
+        // Arrange & Act
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.NoAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo);
+
+        // Assert
+        Assert.False(parameterInfo.HasDefaultValue);
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_WithConstructorArgument_HasDefaultValue()
+    {
+        // Arrange & Act
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.NoAttribute));
+        var parameter = GetParameter(nameof(ArgumentList.DefaultMethod), "withDefaultValue");
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo, parameter);
+
+        // Assert
+        Assert.True(parameterInfo.HasDefaultValue);
+        Assert.NotNull(parameterInfo.DefaultValue);
+        Assert.IsType<int>(parameterInfo.DefaultValue);
+        Assert.NotNull(parameterInfo.RawDefaultValue);
+        Assert.IsType<int>(parameterInfo.RawDefaultValue);
+    }
+
+    [Fact]
+    public void PropertyAsParameterInfoTests_WithConstructorArgument_DoesNotHaveDefaultValue()
+    {
+        // Arrange & Act
+        var propertyInfo = GetProperty(typeof(ArgumentList), nameof(ArgumentList.NoAttribute));
+        var parameter = GetParameter(nameof(ArgumentList.DefaultMethod), nameof(ArgumentList.NoAttribute));
+        var parameterInfo = new PropertyAsParameterInfo(propertyInfo, parameter);
+
+        // Assert
+        Assert.False(parameterInfo.HasDefaultValue);
+    }
+
+    private static PropertyInfo GetProperty(Type containerType, string propertyName)
+        => containerType.GetProperty(propertyName);
+
+    private static ParameterInfo GetParameter(string methodName, string parameterName)
+    {
+        var methodInfo = typeof(ArgumentList).GetMethod(methodName);
+        var parameters = methodInfo.GetParameters();
+        return parameters.Single(p => p.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase));
+    }
+
+    private class ArgumentList
+    {
+        public int NoAttribute { get; set; }
+
+        [Test]
+        public virtual int WithTestAttribute { get; set; }
+
+        [Sample]
+        public int WithSampleAttribute { get; set; }
+
+        public void DefaultMethod(
+            int noAttribute,
+            [Test] int withTestAttribute,
+            [Sample] int withSampleAttribute,
+            int withDefaultValue = 10)
+        { }
+    }
+
+    private class DerivedArgumentList : ArgumentList
+    {
+        [DerivedTest]
+        public override int WithTestAttribute
+        {
+            get => base.WithTestAttribute;
+            set => base.WithTestAttribute = value;
+        }
+    }
+
+    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, Inherited = true)]
+    private class SampleAttribute : Attribute
+    { }
+
+    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, Inherited = true)]
+    private class TestAttribute : Attribute
+    { }
+
+    private class DerivedTestAttribute : TestAttribute
+    { }
+}

+ 665 - 4
src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs

@@ -24,6 +24,7 @@ using Microsoft.AspNetCore.Http.Json;
 using Microsoft.AspNetCore.Http.Metadata;
 using Microsoft.AspNetCore.Testing;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Internal;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Options;
 using Microsoft.Extensions.Primitives;
@@ -216,6 +217,30 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(originalRouteParam, httpContext.Items["input"]);
     }
 
+    private record ParameterListFromRoute(HttpContext HttpContext, int Value);
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromRouteParameterBased_FromParameterList()
+    {
+        const string paramName = "value";
+        const int originalRouteParam = 42;
+
+        static void TestAction([AsParameters] ParameterListFromRoute args)
+        {
+            args.HttpContext.Items.Add("input", args.Value);
+        }
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(originalRouteParam, httpContext.Items["input"]);
+    }
+
     private static void TestOptional(HttpContext httpContext, [FromRoute] int value = 42)
     {
         httpContext.Items.Add("input", value);
@@ -1491,6 +1516,40 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(originalQueryParam, deserializedRouteParam);
     }
 
+    private record ParameterListFromQuery([FromQuery] int Value);
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromQueryParameter_FromParameterList()
+    {
+        // QueryCollection is case sensitve, since we now getting
+        // the parameter name from the Property/Record constructor
+        // we should match the case here
+        const string paramName = "Value";
+        const int originalQueryParam = 42;
+
+        int? deserializedRouteParam = null;
+
+        void TestAction([AsParameters] ParameterListFromQuery args)
+        {
+            deserializedRouteParam = args.Value;
+        }
+
+        var query = new QueryCollection(new Dictionary<string, StringValues>()
+        {
+            [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo)
+        });
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Query = query;
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(originalQueryParam, deserializedRouteParam);
+    }
+
     [Fact]
     public async Task RequestDelegatePopulatesFromHeaderParameterBasedOnParameterName()
     {
@@ -1515,6 +1574,36 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(originalHeaderParam, deserializedRouteParam);
     }
 
+    private record ParameterListFromHeader([FromHeader(Name = "X-Custom-Header")] int Value);
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromHeaderParameter_FromParameterList()
+    {
+        const string customHeaderName = "X-Custom-Header";
+        const int originalHeaderParam = 42;
+
+        int? deserializedRouteParam = null;
+
+        void TestAction([AsParameters] ParameterListFromHeader args)
+        {
+            deserializedRouteParam = args.Value;
+        }
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo);
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(originalHeaderParam, deserializedRouteParam);
+    }
+
+    private record ParametersListWithImplictFromBody(HttpContext HttpContext, TodoStruct Todo);
+
+    private record ParametersListWithExplictFromBody(HttpContext HttpContext, [FromBody] Todo Todo);
+
     public static object[][] ImplicitFromBodyActions
     {
         get
@@ -1534,11 +1623,17 @@ public class RequestDelegateFactoryTests : LoggedTest
                 httpContext.Items.Add("body", todo);
             }
 
+            void TestImpliedFromBodyStruct_ParameterList([AsParameters] ParametersListWithImplictFromBody args)
+            {
+                args.HttpContext.Items.Add("body", args.Todo);
+            }
+
             return new[]
             {
                     new[] { (Action<HttpContext, Todo>)TestImpliedFromBody },
                     new[] { (Action<HttpContext, ITodo>)TestImpliedFromBodyInterface },
                     new object[] { (Action<HttpContext, TodoStruct>)TestImpliedFromBodyStruct },
+                    new object[] { (Action<ParametersListWithImplictFromBody>)TestImpliedFromBodyStruct_ParameterList },
                 };
         }
     }
@@ -1552,10 +1647,16 @@ public class RequestDelegateFactoryTests : LoggedTest
                 httpContext.Items.Add("body", todo);
             }
 
+            void TestExplicitFromBody_ParameterList([AsParameters] ParametersListWithExplictFromBody args)
+            {
+                args.HttpContext.Items.Add("body", args.Todo);
+            }
+
             return new[]
             {
                     new[] { (Action<HttpContext, Todo>)TestExplicitFromBody },
-                };
+                    new object[] { (Action<ParametersListWithExplictFromBody>)TestExplicitFromBody_ParameterList },
+            };
         }
     }
 
@@ -2055,6 +2156,122 @@ public class RequestDelegateFactoryTests : LoggedTest
             throw new NotImplementedException();
     }
 
+    public static object[][] BadArgumentListActions
+    {
+        get
+        {
+            void TestParameterListRecord([AsParameters] BadArgumentListRecord req) { }
+            void TestParameterListClass([AsParameters] BadArgumentListClass req) { }
+            void TestParameterListClassWithMutipleConstructors([AsParameters] BadArgumentListClassMultipleCtors req) { }
+            void TestParameterListAbstractClass([AsParameters] BadAbstractArgumentListClass req) { }
+            void TestParameterListNoPulicConstructorClass([AsParameters] BadNoPublicConstructorArgumentListClass req) { }
+
+            static string GetMultipleContructorsError(Type type)
+                => $"Only a single public parameterized constructor is allowed for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.";
+
+            static string GetAbstractClassError(Type type)
+                => $"The abstract type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}' is not supported.";
+
+            static string GetNoContructorsError(Type type)
+                => $"No public parameterless constructor found for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.";
+
+            static string GetInvalidConstructorError(Type type)
+                => $"The public parameterized constructor must contain only parameters that match the declared public properties for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.";
+
+            return new object[][]
+            {
+                    new object[] { (Action<BadArgumentListRecord>)TestParameterListRecord, GetMultipleContructorsError(typeof(BadArgumentListRecord)) },
+                    new object[] { (Action<BadArgumentListClass>)TestParameterListClass, GetInvalidConstructorError(typeof(BadArgumentListClass)) },
+                    new object[] { (Action<BadArgumentListClassMultipleCtors>)TestParameterListClassWithMutipleConstructors, GetMultipleContructorsError(typeof(BadArgumentListClassMultipleCtors))  },
+                    new object[] { (Action<BadAbstractArgumentListClass>)TestParameterListAbstractClass, GetAbstractClassError(typeof(BadAbstractArgumentListClass)) },
+                    new object[] { (Action<BadNoPublicConstructorArgumentListClass>)TestParameterListNoPulicConstructorClass, GetNoContructorsError(typeof(BadNoPublicConstructorArgumentListClass)) },
+            };
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(BadArgumentListActions))]
+    public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidParameterListConstructor(
+        Delegate @delegate,
+        string errorMessage)
+    {
+        var exception = Assert.Throws<InvalidOperationException>(() => RequestDelegateFactory.Create(@delegate));
+        Assert.Equal(errorMessage, exception.Message);
+    }
+
+    private record BadArgumentListRecord(int Foo)
+    {
+        public BadArgumentListRecord(int foo, int bar)
+            : this(foo)
+        {
+        }
+
+        public int Bar { get; set; }
+    }
+
+    private class BadNoPublicConstructorArgumentListClass
+    {
+        private BadNoPublicConstructorArgumentListClass()
+        { }
+
+        public int Foo { get; set; }
+    }
+
+    private abstract class BadAbstractArgumentListClass
+    {
+        public int Foo { get; set; }
+    }
+
+    private class BadArgumentListClass
+    {
+        public BadArgumentListClass(int foo, string name)
+        {
+        }
+
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+    }
+
+    private class BadArgumentListClassMultipleCtors
+    {
+        public BadArgumentListClassMultipleCtors(int foo)
+        {
+        }
+
+        public BadArgumentListClassMultipleCtors(int foo, int bar)
+        {
+        }
+
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+    }
+
+    private record NestedArgumentListRecord([AsParameters] object NestedParameterList);
+
+    private class ClassWithParametersConstructor
+    {
+        public ClassWithParametersConstructor([AsParameters] object nestedParameterList)
+        {
+            NestedParameterList = nestedParameterList;
+        }
+
+        public object NestedParameterList { get; set; }
+    }
+
+    [Fact]
+    public void BuildRequestDelegateThrowsNotSupportedExceptionForNestedParametersList()
+    {
+        void TestNestedParameterListRecordOnType([AsParameters] NestedArgumentListRecord req) { }
+        void TestNestedParameterListRecordOnArgument([AsParameters] ClassWithParametersConstructor req) { }
+
+        Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestNestedParameterListRecordOnType));
+        Assert.Throws<NotSupportedException>(() => RequestDelegateFactory.Create(TestNestedParameterListRecordOnArgument));
+    }
+
+    private record ParametersListWithImplictFromService(HttpContext HttpContext, IMyService MyService);
+
+    private record ParametersListWithExplictFromService(HttpContext HttpContext, [FromService] MyService MyService);
+
     public static object[][] ExplicitFromServiceActions
     {
         get
@@ -2064,6 +2281,11 @@ public class RequestDelegateFactoryTests : LoggedTest
                 httpContext.Items.Add("service", myService);
             }
 
+            void TestExplicitFromService_FromParameterList([AsParameters] ParametersListWithExplictFromService args)
+            {
+                args.HttpContext.Items.Add("service", args.MyService);
+            }
+
             void TestExplicitFromIEnumerableService(HttpContext httpContext, [FromService] IEnumerable<MyService> myServices)
             {
                 httpContext.Items.Add("service", myServices.Single());
@@ -2077,6 +2299,7 @@ public class RequestDelegateFactoryTests : LoggedTest
             return new object[][]
             {
                     new[] { (Action<HttpContext, MyService>)TestExplicitFromService },
+                    new object[] { (Action<ParametersListWithExplictFromService>)TestExplicitFromService_FromParameterList },
                     new[] { (Action<HttpContext, IEnumerable<MyService>>)TestExplicitFromIEnumerableService },
                     new[] { (Action<HttpContext, MyService, IEnumerable<MyService>>)TestExplicitMultipleFromService },
             };
@@ -2092,6 +2315,11 @@ public class RequestDelegateFactoryTests : LoggedTest
                 httpContext.Items.Add("service", myService);
             }
 
+            void TestImpliedFromService_FromParameterList([AsParameters] ParametersListWithImplictFromService args)
+            {
+                args.HttpContext.Items.Add("service", args.MyService);
+            }
+
             void TestImpliedIEnumerableFromService(HttpContext httpContext, IEnumerable<MyService> myServices)
             {
                 httpContext.Items.Add("service", myServices.Single());
@@ -2105,6 +2333,7 @@ public class RequestDelegateFactoryTests : LoggedTest
             return new object[][]
             {
                     new[] { (Action<HttpContext, IMyService>)TestImpliedFromService },
+                    new object[] { (Action<ParametersListWithImplictFromService>)TestImpliedFromService_FromParameterList },
                     new[] { (Action<HttpContext, IEnumerable<MyService>>)TestImpliedIEnumerableFromService },
                     new[] { (Action<HttpContext, MyService>)TestImpliedFromServiceBasedOnContainer },
             };
@@ -2198,6 +2427,32 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Same(httpContext, httpContextArgument);
     }
 
+    private record ParametersListWithHttpContext(
+        HttpContext HttpContext,
+        ClaimsPrincipal User,
+        HttpRequest HttpRequest,
+        HttpResponse HttpResponse);
+
+    [Fact]
+    public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute_FromParameterList()
+    {
+        HttpContext? httpContextArgument = null;
+
+        void TestAction([AsParameters] ParametersListWithHttpContext args)
+        {
+            httpContextArgument = args.HttpContext;
+        }
+
+        var httpContext = CreateHttpContext();
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Same(httpContext, httpContextArgument);
+    }
+
     [Fact]
     public async Task RequestDelegatePassHttpContextRequestAbortedAsCancellationToken()
     {
@@ -2243,6 +2498,27 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(httpContext.User, userArgument);
     }
 
+    [Fact]
+    public async Task RequestDelegatePassHttpContextUserAsClaimsPrincipal_FromParameterList()
+    {
+        ClaimsPrincipal? userArgument = null;
+
+        void TestAction([AsParameters] ParametersListWithHttpContext args)
+        {
+            userArgument = args.User;
+        }
+
+        var httpContext = CreateHttpContext();
+        httpContext.User = new ClaimsPrincipal();
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(httpContext.User, userArgument);
+    }
+
     [Fact]
     public async Task RequestDelegatePassHttpContextRequestAsHttpRequest()
     {
@@ -2263,6 +2539,26 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(httpContext.Request, httpRequestArgument);
     }
 
+    [Fact]
+    public async Task RequestDelegatePassHttpContextRequestAsHttpRequest_FromParameterList()
+    {
+        HttpRequest? httpRequestArgument = null;
+
+        void TestAction([AsParameters] ParametersListWithHttpContext args)
+        {
+            httpRequestArgument = args.HttpRequest;
+        }
+
+        var httpContext = CreateHttpContext();
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Request, httpRequestArgument);
+    }
+
     [Fact]
     public async Task RequestDelegatePassesHttpContextRresponseAsHttpResponse()
     {
@@ -2283,6 +2579,26 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(httpContext.Response, httpResponseArgument);
     }
 
+    [Fact]
+    public async Task RequestDelegatePassesHttpContextRresponseAsHttpResponse_FromParameterList()
+    {
+        HttpResponse? httpResponseArgument = null;
+
+        void TestAction([AsParameters] ParametersListWithHttpContext args)
+        {
+            httpResponseArgument = args.HttpResponse;
+        }
+
+        var httpContext = CreateHttpContext();
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(httpContext.Response, httpResponseArgument);
+    }
+
     public static IEnumerable<object[]> ComplexResult
     {
         get
@@ -2778,7 +3094,7 @@ public class RequestDelegateFactoryTests : LoggedTest
             var log = Assert.Single(logs);
             Assert.Equal(LogLevel.Debug, log.LogLevel);
             Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId);
-            var expectedType = paramName == "age" ? "int age" : "string name";
+            var expectedType = paramName == "age" ? "int age" : $"string name";
             Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from route or query string.", log.Message);
         }
         else
@@ -2850,7 +3166,7 @@ public class RequestDelegateFactoryTests : LoggedTest
             var log = Assert.Single(logs);
             Assert.Equal(LogLevel.Debug, log.LogLevel);
             Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId);
-            var expectedType = paramName == "age" ? "int age" : "string name";
+            var expectedType = paramName == "age" ? "int age" : $"string name";
             Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from query string.", log.Message);
         }
         else
@@ -4218,6 +4534,312 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Equal(400, badHttpRequestException.StatusCode);
     }
 
+    private record struct ParameterListRecordStruct(HttpContext HttpContext, [FromRoute] int Value);
+
+    private record ParameterListRecordClass(HttpContext HttpContext, [FromRoute] int Value);
+
+    private record ParameterListRecordWithoutPositionalParameters
+    {
+        public HttpContext? HttpContext { get; set; }
+
+        [FromRoute]
+        public int Value { get; set; }
+    }
+
+    private struct ParameterListStruct
+    {
+        public HttpContext HttpContext { get; set; }
+
+        [FromRoute]
+        public int Value { get; set; }
+    }
+
+    private struct ParameterListMutableStruct
+    {
+        public ParameterListMutableStruct()
+        {
+            Value = -1;
+            HttpContext = default!;
+        }
+
+        public HttpContext HttpContext { get; set; }
+
+        [FromRoute]
+        public int Value { get; set; }
+    }
+
+    private class ParameterListStructWithParameterizedContructor
+    {
+        public ParameterListStructWithParameterizedContructor(HttpContext httpContext)
+        {
+            HttpContext = httpContext;
+            Value = 42;
+        }
+
+        public HttpContext HttpContext { get; set; }
+
+        public int Value { get; set; }
+    }
+
+    private struct ParameterListStructWithMultipleParameterizedContructor
+    {
+        public ParameterListStructWithMultipleParameterizedContructor(HttpContext httpContext)
+        {
+            HttpContext = httpContext;
+            Value = 10;
+        }
+
+        public ParameterListStructWithMultipleParameterizedContructor(HttpContext httpContext, [FromHeader(Name ="Value")] int value)
+        {
+            HttpContext = httpContext;
+            Value = value;
+        }
+
+        public HttpContext HttpContext { get; set; }
+
+        [FromRoute]
+        public int Value { get; set; }
+    }
+
+    private class ParameterListClass
+    {
+        public HttpContext? HttpContext { get; set; }
+
+        [FromRoute]
+        public int Value { get; set; }
+    }
+
+    private class ParameterListClassWithParameterizedContructor
+    {
+        public ParameterListClassWithParameterizedContructor(HttpContext httpContext)
+        {
+            HttpContext = httpContext;
+            Value = 42;
+        }
+
+        public HttpContext HttpContext { get; set; }
+
+        public int Value { get; set; }
+    }
+
+    public static object[][] FromParameterListActions
+    {
+        get
+        {
+            void TestParameterListRecordStruct([AsParameters] ParameterListRecordStruct args)
+            {
+                args.HttpContext.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListRecordClass([AsParameters] ParameterListRecordClass args)
+            {
+                args.HttpContext.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListRecordWithoutPositionalParameters([AsParameters] ParameterListRecordWithoutPositionalParameters args)
+            {
+                args.HttpContext!.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListStruct([AsParameters] ParameterListStruct args)
+            {
+                args.HttpContext.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListMutableStruct([AsParameters] ParameterListMutableStruct args)
+            {
+                args.HttpContext.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListStructWithParameterizedContructor([AsParameters] ParameterListStructWithParameterizedContructor args)
+            {
+                args.HttpContext.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListStructWithMultipleParameterizedContructor([AsParameters] ParameterListStructWithMultipleParameterizedContructor args)
+            {
+                args.HttpContext.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListClass([AsParameters] ParameterListClass args)
+            {
+                args.HttpContext!.Items.Add("input", args.Value);
+            }
+
+            void TestParameterListClassWithParameterizedContructor([AsParameters] ParameterListClassWithParameterizedContructor args)
+            {
+                args.HttpContext.Items.Add("input", args.Value);
+            }
+
+            return new[]
+            {
+                new object[] { (Action<ParameterListRecordStruct>)TestParameterListRecordStruct },
+                new object[] { (Action<ParameterListRecordClass>)TestParameterListRecordClass },
+                new object[] { (Action<ParameterListRecordWithoutPositionalParameters>)TestParameterListRecordWithoutPositionalParameters },
+                new object[] { (Action<ParameterListStruct>)TestParameterListStruct },
+                new object[] { (Action<ParameterListMutableStruct>)TestParameterListMutableStruct },
+                new object[] { (Action<ParameterListStructWithParameterizedContructor>)TestParameterListStructWithParameterizedContructor },
+                new object[] { (Action<ParameterListStructWithMultipleParameterizedContructor>)TestParameterListStructWithMultipleParameterizedContructor },
+                new object[] { (Action<ParameterListClass>)TestParameterListClass },
+                new object[] { (Action<ParameterListClassWithParameterizedContructor>)TestParameterListClassWithParameterizedContructor },
+            };
+        }
+    }
+
+    [Theory]
+    [MemberData(nameof(FromParameterListActions))]
+    public async Task RequestDelegatePopulatesFromParameterList(Delegate action)
+    {
+        const string paramName = "value";
+        const int originalRouteParam = 42;
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo);
+
+        var factoryResult = RequestDelegateFactory.Create(action);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(originalRouteParam, httpContext.Items["input"]);
+    }
+
+    private record struct SampleParameterList(int Foo);
+    private record struct AdditionalSampleParameterList(int Bar);
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromMultipleParameterLists()
+    {
+        const int foo = 1;
+        const int bar = 2;
+
+        void TestAction(HttpContext context, [AsParameters] SampleParameterList args, [AsParameters] AdditionalSampleParameterList args2)
+        {
+            context.Items.Add("foo", args.Foo);
+            context.Items.Add("bar", args2.Bar);
+        }
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.RouteValues[nameof(SampleParameterList.Foo)] = foo.ToString(NumberFormatInfo.InvariantInfo);
+        httpContext.Request.RouteValues[nameof(AdditionalSampleParameterList.Bar)] = bar.ToString(NumberFormatInfo.InvariantInfo);
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(foo, httpContext.Items["foo"]);
+        Assert.Equal(bar, httpContext.Items["bar"]);
+    }
+
+    [Fact]
+    public void RequestDelegateThrowsWhenParameterNameConflicts()
+    {
+        void TestAction(HttpContext context, [AsParameters] SampleParameterList args, [AsParameters] SampleParameterList args2)
+        {
+            context.Items.Add("foo", args.Foo);
+        }
+        var httpContext = CreateHttpContext();
+
+        var exception = Assert.Throws<ArgumentException>(() => RequestDelegateFactory.Create(TestAction));
+        Assert.Contains("An item with the same key has already been added. Key: Foo", exception.Message);
+    }
+
+    private class ParameterListWithReadOnlyProperties
+    {
+        public ParameterListWithReadOnlyProperties()
+        {
+            ReadOnlyValue = 1;
+        }
+
+        public int Value { get; set; }
+
+        public int ConstantValue => 1;
+
+        public int ReadOnlyValue { get; }
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromParameterListAndSkipReadOnlyProperties()
+    {
+        const int routeParamValue = 42;
+        var expectedInput = new ParameterListWithReadOnlyProperties() { Value = routeParamValue };
+
+        void TestAction(HttpContext context, [AsParameters] ParameterListWithReadOnlyProperties args)
+        {
+            context.Items.Add("input", args);
+        }
+
+        var httpContext = CreateHttpContext();
+        httpContext.Request.RouteValues[nameof(ParameterListWithReadOnlyProperties.Value)] = routeParamValue.ToString(NumberFormatInfo.InvariantInfo);
+        httpContext.Request.RouteValues[nameof(ParameterListWithReadOnlyProperties.ConstantValue)] = routeParamValue.ToString(NumberFormatInfo.InvariantInfo);
+        httpContext.Request.RouteValues[nameof(ParameterListWithReadOnlyProperties.ReadOnlyValue)] = routeParamValue.ToString(NumberFormatInfo.InvariantInfo);
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        var input = Assert.IsType<ParameterListWithReadOnlyProperties>(httpContext.Items["input"]);
+        Assert.Equal(expectedInput.Value, input.Value);
+        Assert.Equal(expectedInput.ConstantValue, input.ConstantValue);
+        Assert.Equal(expectedInput.ReadOnlyValue, input.ReadOnlyValue);
+    }
+
+    private record ParameterListRecordWitDefaultValue(HttpContext HttpContext, [FromRoute] int Value = 42);
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromParameterListRecordUsesDefaultValue()
+    {
+        const int expectedValue = 42;
+
+        void TestAction([AsParameters] ParameterListRecordWitDefaultValue args)
+        {
+            args.HttpContext.Items.Add("input", args.Value);
+        }
+
+        var httpContext = CreateHttpContext();
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(expectedValue, httpContext.Items["input"]);
+    }
+
+    private class ParameterListWitDefaultValue
+    {
+        public ParameterListWitDefaultValue(HttpContext httpContext, [FromRoute]int value = 42)
+        {
+            HttpContext = httpContext;
+            Value = value;
+        }
+
+        public HttpContext HttpContext { get; }
+        public int Value { get; }
+    }
+
+    [Fact]
+    public async Task RequestDelegatePopulatesFromParameterListUsesDefaultValue()
+    {
+        const int expectedValue = 42;
+
+        void TestAction([AsParameters] ParameterListWitDefaultValue args)
+        {
+            args.HttpContext.Items.Add("input", args.Value);
+        }
+
+        var httpContext = CreateHttpContext();
+
+        var factoryResult = RequestDelegateFactory.Create(TestAction);
+        var requestDelegate = factoryResult.RequestDelegate;
+
+        await requestDelegate(httpContext);
+
+        Assert.Equal(expectedValue, httpContext.Items["input"]);
+    }
+
     [Fact]
     public async Task RequestDelegateFactory_InvokesFiltersButNotHandler_OnArgumentError()
     {
@@ -5278,6 +5900,29 @@ public class RequestDelegateFactoryTests : LoggedTest
         Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter });
     }
 
+    [Fact]
+    public void Create_CombinesPropertiesAsParameterMetadata_AndTopLevelParameter()
+    {
+        // Arrange
+        var @delegate = ([AsParameters] AddsCustomParameterMetadata param1) => new CountsDefaultEndpointMetadataResult();
+        var options = new RequestDelegateFactoryOptions
+        {
+            InitialEndpointMetadata = new List<object>
+            {
+                new CustomEndpointMetadata { Source = MetadataSource.Caller }
+            }
+        };
+
+        // Act
+        var result = RequestDelegateFactory.Create(@delegate, options);
+
+        // Assert
+        Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Parameter });
+        Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: "param1" });
+        Assert.Contains(result.EndpointMetadata, m => m is CustomEndpointMetadata { Source: MetadataSource.Property });
+        Assert.Contains(result.EndpointMetadata, m => m is ParameterNameMetadata { Name: nameof(AddsCustomParameterMetadata.Data) });
+    }
+
     [Fact]
     public void Create_CombinesAllMetadata_InCorrectOrder()
     {
@@ -5491,8 +6136,23 @@ public class RequestDelegateFactoryTests : LoggedTest
         public Task ExecuteAsync(HttpContext httpContext) => throw new NotImplementedException();
     }
 
+    private class AddsCustomParameterMetadataAsProperty : IEndpointParameterMetadataProvider, IEndpointMetadataProvider
+    {
+        public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext)
+        {
+            parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name });
+        }
+
+        public static void PopulateMetadata(EndpointMetadataContext context)
+        {
+            context.EndpointMetadata?.Add(new CustomEndpointMetadata { Source = MetadataSource.Property });
+        }
+    }
+
     private class AddsCustomParameterMetadata : IEndpointParameterMetadataProvider, IEndpointMetadataProvider
     {
+        public AddsCustomParameterMetadataAsProperty? Data { get; set; }
+
         public static void PopulateMetadata(EndpointParameterMetadataContext parameterContext)
         {
             parameterContext.EndpointMetadata?.Add(new ParameterNameMetadata { Name = parameterContext.Parameter?.Name });
@@ -5540,7 +6200,8 @@ public class RequestDelegateFactoryTests : LoggedTest
     {
         Caller,
         Parameter,
-        ReturnType
+        ReturnType,
+        Property
     }
 
     private class Todo : ITodo

+ 7 - 9
src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

@@ -116,20 +116,18 @@ internal sealed class EndpointMetadataApiDescriptionProvider : IApiDescriptionPr
 
         var hasBodyOrFormFileParameter = false;
 
-        foreach (var parameter in methodInfo.GetParameters())
+        foreach (var parameter in PropertyAsParameterInfo.Flatten(methodInfo.GetParameters(), ParameterBindingMethodCache))
         {
             var parameterDescription = CreateApiParameterDescription(parameter, routeEndpoint.RoutePattern, disableInferredBody);
 
-            if (parameterDescription is null)
+            if (parameterDescription is { })
             {
-                continue;
-            }
-
-            apiDescription.ParameterDescriptions.Add(parameterDescription);
+                apiDescription.ParameterDescriptions.Add(parameterDescription);
 
-            hasBodyOrFormFileParameter |=
-                parameterDescription.Source == BindingSource.Body ||
-                parameterDescription.Source == BindingSource.FormFile;
+                hasBodyOrFormFileParameter |=
+                    parameterDescription.Source == BindingSource.Body ||
+                    parameterDescription.Source == BindingSource.FormFile;
+            }
         }
 
         // Get IAcceptsMetadata.

+ 1 - 0
src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj

@@ -11,6 +11,7 @@
 
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)RoslynUtils\TypeHelper.cs" />
+    <Compile Include="$(SharedSourceRoot)PropertyAsParameterInfo.cs" LinkBase="Shared" />
   </ItemGroup>
 
   <ItemGroup>

+ 6 - 0
src/Mvc/Mvc.ApiExplorer/test/DefaultApiDescriptionProviderTest.cs

@@ -2382,6 +2382,9 @@ public class DefaultApiDescriptionProviderTest
         [FromHeader]
         public string UserId { get; set; }
 
+        [FromServices]
+        public ITestService TestService { get; set; }
+
         [ModelBinder]
         public string Comments { get; set; }
 
@@ -2475,6 +2478,9 @@ public class DefaultApiDescriptionProviderTest
         [FromHeader]
         public string UserId { get; set; }
 
+        [FromServices]
+        public ITestService TestService { get; set; }
+
         public string Comments { get; set; }
     }
 

+ 80 - 0
src/Mvc/Mvc.ApiExplorer/test/EndpointMetadataApiDescriptionProviderTest.cs

@@ -447,6 +447,48 @@ public class EndpointMetadataApiDescriptionProviderTest
 
 #nullable disable
 
+    [Fact]
+    public void AddsMultipleParametersFromParametersAttribute()
+    {
+        static void AssertParameters(ApiDescription apiDescription)
+        {
+            Assert.Collection(
+                apiDescription.ParameterDescriptions,
+                param =>
+                {
+                    Assert.Equal("Foo", param.Name);
+                    Assert.Equal(typeof(int), param.ModelMetadata.ModelType);
+                    Assert.Equal(BindingSource.Path, param.Source);
+                    Assert.True(param.IsRequired);
+                },
+                param =>
+                {
+                    Assert.Equal("Bar", param.Name);
+                    Assert.Equal(typeof(int), param.ModelMetadata.ModelType);
+                    Assert.Equal(BindingSource.Query, param.Source);
+                    Assert.True(param.IsRequired);
+                },
+                param =>
+                {
+                    Assert.Equal("FromBody", param.Name);
+                    Assert.Equal(typeof(InferredJsonClass), param.Type);
+                    Assert.Equal(typeof(InferredJsonClass), param.ModelMetadata.ModelType);
+                    Assert.Equal(BindingSource.Body, param.Source);
+                    Assert.False(param.IsRequired);
+                }
+            );
+        }
+
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListClass req) => { }));
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListClassWithReadOnlyProperties req) => { }));
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListStruct req) => { }));
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListRecord req) => { }));
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListRecordStruct req) => { }));
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListRecordWithoutPositionalParameters req) => { }));
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListRecordWithoutAttributes req) => { }, "/{foo}"));
+        AssertParameters(GetApiDescription(([AsParameters] ArgumentListRecordWithoutAttributes req) => { }, "/{Foo}"));
+    }
+
     [Fact]
     public void TestParameterIsRequired()
     {
@@ -1323,6 +1365,44 @@ public class EndpointMetadataApiDescriptionProviderTest
             throw new NotImplementedException();
     }
 
+    private record ArgumentListRecord([FromRoute] int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);
+
+    private record struct ArgumentListRecordStruct([FromRoute] int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);
+
+    private record ArgumentListRecordWithoutAttributes(int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);
+
+    private record ArgumentListRecordWithoutPositionalParameters
+    {
+        [FromRoute]
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+        public InferredJsonClass FromBody { get; set; }
+        public HttpContext Context { get; set; }
+    }
+
+    private class ArgumentListClass
+    {
+        [FromRoute]
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+        public InferredJsonClass FromBody { get; set; }
+        public HttpContext Context { get; set; }
+    }
+
+    private class ArgumentListClassWithReadOnlyProperties : ArgumentListClass
+    {
+        public int ReadOnly { get; }
+    }
+
+    private struct ArgumentListStruct
+    {
+        [FromRoute]
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+        public InferredJsonClass FromBody { get; set; }
+        public HttpContext Context { get; set; }
+    }
+
     private class TestServiceProvider : IServiceProvider
     {
         public void Dispose()

+ 2 - 2
src/Mvc/Mvc.Core/src/FromServicesAttribute.cs

@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
 namespace Microsoft.AspNetCore.Mvc;
 
 /// <summary>
-/// Specifies that an action parameter should be bound using the request services.
+/// Specifies that a parameter or property should be bound using the request services.
 /// </summary>
 /// <example>
 /// In this example an implementation of IProductModelRequestService is registered as a service.
@@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Mvc;
 /// }
 /// </code>
 /// </example>
-[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
+[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
 public class FromServicesAttribute : Attribute, IBindingSourceMetadata, IFromServiceMetadata
 {
     /// <inheritdoc />

+ 26 - 2
src/Mvc/Mvc.Core/test/ApplicationModels/DefaultApplicationModelProviderTest.cs

@@ -93,6 +93,15 @@ public class DefaultApplicationModelProviderTest
                 Assert.Empty(property.Attributes);
             },
             property =>
+            {
+                Assert.Equal(nameof(ModelBinderController.Service), property.PropertyName);
+                Assert.Equal(BindingSource.Services, property.BindingInfo.BindingSource);
+                Assert.Same(controllerModel, property.Controller);
+
+                var attribute = Assert.Single(property.Attributes);
+                Assert.IsType<FromServicesAttribute>(attribute); ;
+            },
+            property =>
             {
                 Assert.Equal(nameof(ModelBinderController.Unbound), property.PropertyName);
                 Assert.Null(property.BindingInfo);
@@ -104,7 +113,7 @@ public class DefaultApplicationModelProviderTest
     public void OnProvidersExecuting_ReadsBindingSourceForPropertiesFromModelMetadata()
     {
         // Arrange
-        var detailsProvider = new BindingSourceMetadataProvider(typeof(string), BindingSource.Services);
+        var detailsProvider = new BindingSourceMetadataProvider(typeof(string), BindingSource.Special);
         var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(new[] { detailsProvider });
         var typeInfo = typeof(ModelBinderController).GetTypeInfo();
         var provider = new TestApplicationModelProvider(new MvcOptions(), modelMetadataProvider);
@@ -137,9 +146,18 @@ public class DefaultApplicationModelProviderTest
             },
             property =>
             {
-                Assert.Equal(nameof(ModelBinderController.Unbound), property.PropertyName);
+                Assert.Equal(nameof(ModelBinderController.Service), property.PropertyName);
                 Assert.Equal(BindingSource.Services, property.BindingInfo.BindingSource);
                 Assert.Same(controllerModel, property.Controller);
+
+                var attribute = Assert.Single(property.Attributes);
+                Assert.IsType<FromServicesAttribute>(attribute);
+            },
+            property =>
+            {
+                Assert.Equal(nameof(ModelBinderController.Unbound), property.PropertyName);
+                Assert.Equal(BindingSource.Special, property.BindingInfo.BindingSource);
+                Assert.Same(controllerModel, property.Controller);
             });
     }
 
@@ -1743,6 +1761,9 @@ public class DefaultApplicationModelProviderTest
     {
     }
 
+    public interface ITestService
+    { }
+
     public class ModelBinderController
     {
         [FromQuery]
@@ -1750,6 +1771,9 @@ public class DefaultApplicationModelProviderTest
 
         public string Unbound { get; set; }
 
+        [FromServices]
+        public ITestService Service { get; set; }
+
         public IFormFile FormFile { get; set; }
 
         public IActionResult PostAction([FromQuery] string fromQuery, IFormFileCollection formFileCollection, string unbound) => null;

+ 15 - 1
src/Mvc/Mvc.RazorPages/test/ApplicationModels/DefaultPageApplicationModelProviderTest.cs

@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Reflection;
@@ -353,6 +353,14 @@ public class DefaultPageApplicationModelProviderTest
                 Assert.Equal(name, property.PropertyName);
                 Assert.NotNull(property.BindingInfo);
                 Assert.Equal(BindingSource.Query, property.BindingInfo.BindingSource);
+            },
+            property =>
+            {
+                var name = nameof(TestPageModel.TestService);
+                Assert.Equal(modelType.GetProperty(name), property.PropertyInfo);
+                Assert.Equal(name, property.PropertyName);
+                Assert.NotNull(property.BindingInfo);
+                Assert.Equal(BindingSource.Services, property.BindingInfo.BindingSource);
             });
     }
 
@@ -1058,6 +1066,9 @@ public class DefaultPageApplicationModelProviderTest
         public override Task ExecuteAsync() => throw new NotImplementedException();
     }
 
+    public interface ITestService
+    { }
+
     [PageModel]
     private class TestPageModel
     {
@@ -1066,6 +1077,9 @@ public class DefaultPageApplicationModelProviderTest
         [FromQuery]
         public string Property2 { get; set; }
 
+        [FromServices]
+        public ITestService TestService { get; set; }
+
         public void OnGetUser() { }
     }
 

+ 17 - 1
src/Mvc/Mvc.RazorPages/test/Infrastructure/PageBinderFactoryTest.cs

@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.ComponentModel.DataAnnotations;
@@ -838,6 +838,9 @@ public class PageBinderFactoryTest
         }
     }
 
+    private interface ITestService
+    { }
+
     private class PageModelWithNoBoundProperties : PageModel
     {
     }
@@ -855,6 +858,9 @@ public class PageBinderFactoryTest
         [FromQuery]
         protected string FromQuery { get; set; }
 
+        [FromServices]
+        protected ITestService FromService { get; set; }
+
         [FromRoute]
         public static int FromRoute { get; set; }
 
@@ -869,6 +875,9 @@ public class PageBinderFactoryTest
         [FromQuery]
         protected string FromQuery { get; set; }
 
+        [FromServices]
+        protected ITestService FromService { get; set; }
+
         [FromRoute]
         public static int FromRoute { get; set; }
     }
@@ -898,6 +907,9 @@ public class PageBinderFactoryTest
         [FromForm]
         public string PropertyWithNoValue { get; set; }
 
+        [FromServices]
+        public ITestService FromService { get; set; }
+
         public override Task ExecuteAsync() => Task.FromResult(0);
     }
 
@@ -911,6 +923,10 @@ public class PageBinderFactoryTest
 
         [FromForm]
         public string PropertyWithNoValue { get; set; }
+
+        [FromServices]
+        public ITestService FromService { get; set; }
+
     }
 
     private class PageModelWithDefaultValue

+ 1 - 0
src/Mvc/test/Mvc.FunctionalTests/RequestServicesTestBase.cs

@@ -31,6 +31,7 @@ public abstract class RequestServicesTestBase<TStartup> : IClassFixture<MvcTestF
     [InlineData("http://localhost/RequestScopedService/FromView")]
     [InlineData("http://localhost/RequestScopedService/FromViewComponent")]
     [InlineData("http://localhost/RequestScopedService/FromActionArgument")]
+    [InlineData("http://localhost/RequestScopedService/FromProperty")]
     public async Task RequestServices(string url)
     {
         for (var i = 0; i < 2; i++)

+ 2 - 2
src/Mvc/test/Mvc.IntegrationTests/ServicesModelBinderIntegrationTest.cs

@@ -332,6 +332,7 @@ public class ServicesModelBinderIntegrationTest
 
     private class Person
     {
+        [FromServices]
         public ITypeActivatorCache Service { get; set; }
     }
 
@@ -348,8 +349,7 @@ public class ServicesModelBinderIntegrationTest
         // Similar to a custom IBindingSourceMetadata implementation or [ModelBinder] subclass on a custom service.
         var metadataProvider = new TestModelMetadataProvider();
         metadataProvider
-            .ForProperty<Person>(nameof(Person.Service))
-            .BindingDetails(binding => binding.BindingSource = BindingSource.Services);
+            .ForProperty<Person>(nameof(Person.Service));
 
         var testContext = ModelBindingTestHelper.GetTestContext(metadataProvider: metadataProvider);
         var modelState = testContext.ModelState;

+ 9 - 0
src/Mvc/test/WebSites/BasicWebSite/Controllers/RequestScopedServiceController.cs

@@ -45,4 +45,13 @@ public class RequestScopedServiceController : Controller
     {
         return requestIdService.RequestId;
     }
+
+    [FromServices]
+    public RequestIdService RequestIdService { get; set; }
+
+    [HttpGet]
+    public string FromProperty()
+    {
+        return RequestIdService.RequestId;
+    }
 }

+ 1 - 0
src/OpenApi/src/Microsoft.AspNetCore.OpenApi.csproj

@@ -17,6 +17,7 @@
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)RoslynUtils\TypeHelper.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)ObjectMethodExecutor\**\*.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)PropertyAsParameterInfo.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)ParameterBindingMethodCache.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)TypeNameHelper\TypeNameHelper.cs" LinkBase="Shared" />
   </ItemGroup>

+ 3 - 2
src/OpenApi/src/OpenApiGenerator.cs

@@ -250,7 +250,8 @@ internal sealed class OpenApiGenerator
         var hasFormOrBodyParameter = false;
         ParameterInfo? requestBodyParameter = null;
 
-        foreach (var parameter in methodInfo.GetParameters())
+        var parameters = PropertyAsParameterInfo.Flatten(methodInfo.GetParameters(), ParameterBindingMethodCache);
+        foreach (var parameter in parameters)
         {
             var (bodyOrFormParameter, _) = GetOpenApiParameterLocation(parameter, pattern, false);
             hasFormOrBodyParameter |= bodyOrFormParameter;
@@ -348,7 +349,7 @@ internal sealed class OpenApiGenerator
 
     private List<OpenApiParameter> GetOpenApiParameters(MethodInfo methodInfo, EndpointMetadataCollection metadata, RoutePattern pattern, bool disableInferredBody)
     {
-        var parameters = methodInfo.GetParameters();
+        var parameters = PropertyAsParameterInfo.Flatten(methodInfo.GetParameters(), ParameterBindingMethodCache);
         var openApiParameters = new List<OpenApiParameter>();
 
         foreach (var parameter in parameters)

+ 79 - 1
src/OpenApi/test/OpenApiGeneratorTests.cs

@@ -352,9 +352,49 @@ public class OpenApiOperationGeneratorTests
         Assert.Equal("object", fromBodyParam.Content.First().Value.Schema.Type);
         Assert.True(fromBodyParam.Required);
     }
-
 #nullable disable
 
+    [Fact]
+    public void AddsMultipleParametersFromParametersAttribute()
+    {
+        static void AssertParameters(OpenApiOperation operation)
+        {
+            Assert.Collection(
+                operation.Parameters,
+                param =>
+                {
+                    Assert.Equal("Foo", param.Name);
+                    Assert.Equal("integer", param.Schema.Type);
+                    Assert.Equal(ParameterLocation.Path, param.In);
+                    Assert.True(param.Required);
+                },
+                param =>
+                {
+                    Assert.Equal("Bar", param.Name);
+                    Assert.Equal("integer", param.Schema.Type);
+                    Assert.Equal(ParameterLocation.Query, param.In);
+                    Assert.True(param.Required);
+                },
+                param =>
+                {
+                    Assert.Equal("FromBody", param.Name);
+                    var fromBodyParam = operation.RequestBody;
+                    Assert.Equal("object", fromBodyParam.Content.First().Value.Schema.Type);
+                    Assert.False(fromBodyParam.Required);
+                }
+            );
+        }
+
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListClass req) => { }));
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListClassWithReadOnlyProperties req) => { }));
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListStruct req) => { }));
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecord req) => { }));
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordStruct req) => { }));
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordWithoutPositionalParameters req) => { }));
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordWithoutAttributes req) => { }, "/{foo}"));
+        AssertParameters(GetOpenApiOperation(([AsParameters] ArgumentListRecordWithoutAttributes req) => { }, "/{Foo}"));
+    }
+
     [Fact]
     public void TestParameterIsRequired()
     {
@@ -802,4 +842,42 @@ public class OpenApiOperationGeneratorTests
         public static bool TryParse(string value, out BindAsyncRecord result) =>
             throw new NotImplementedException();
     }
+
+    private record ArgumentListRecord([FromRoute] int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);
+
+    private record struct ArgumentListRecordStruct([FromRoute] int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);
+
+    private record ArgumentListRecordWithoutAttributes(int Foo, int Bar, InferredJsonClass FromBody, HttpContext context);
+
+    private record ArgumentListRecordWithoutPositionalParameters
+    {
+        [FromRoute]
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+        public InferredJsonClass FromBody { get; set; }
+        public HttpContext Context { get; set; }
+    }
+
+    private class ArgumentListClass
+    {
+        [FromRoute]
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+        public InferredJsonClass FromBody { get; set; }
+        public HttpContext Context { get; set; }
+    }
+
+    private class ArgumentListClassWithReadOnlyProperties : ArgumentListClass
+    {
+        public int ReadOnly { get; }
+    }
+
+    private struct ArgumentListStruct
+    {
+        [FromRoute]
+        public int Foo { get; set; }
+        public int Bar { get; set; }
+        public InferredJsonClass FromBody { get; set; }
+        public HttpContext Context { get; set; }
+    }
 }

+ 134 - 0
src/Shared/ParameterBindingMethodCache.cs

@@ -34,6 +34,7 @@ internal sealed class ParameterBindingMethodCache
     // Since this is shared source, the cache won't be shared between RequestDelegateFactory and the ApiDescriptionProvider sadly :(
     private readonly ConcurrentDictionary<Type, Func<ParameterExpression, Expression, Expression>?> _stringMethodCallCache = new();
     private readonly ConcurrentDictionary<Type, (Func<ParameterInfo, Expression>?, int)> _bindAsyncMethodCallCache = new();
+    private readonly ConcurrentDictionary<Type, (ConstructorInfo?, ConstructorParameter[])> _constructorCache = new();
 
     // If IsDynamicCodeSupported is false, we can't use the static Enum.TryParse<T> since there's no easy way for
     // this code to generate the specific instantiation for any enums used
@@ -272,6 +273,102 @@ internal sealed class ParameterBindingMethodCache
         }
     }
 
+    public (ConstructorInfo?, ConstructorParameter[]) FindConstructor(Type type)
+    {
+        static (ConstructorInfo? constructor, ConstructorParameter[] parameters) Finder(Type type)
+        {
+            var constructor = GetConstructor(type);
+
+            if (constructor is null || constructor.GetParameters().Length == 0)
+            {
+                return (constructor, Array.Empty<ConstructorParameter>());
+            }
+
+            var properties = type.GetProperties();
+            var lookupTable = new Dictionary<ParameterLookupKey, PropertyInfo>(properties.Length);
+            for (var i = 0; i < properties.Length; i++)
+            {
+                lookupTable.Add(new ParameterLookupKey(properties[i].Name, properties[i].PropertyType), properties[i]);
+            }
+
+            // This behavior diverge from the JSON serialization
+            // since we don't have an attribute, eg. JsonConstructor,
+            // we need to be very restrictive about the ctor
+            // and only accept if the parameterized ctor has
+            // only arguments that we can match (Type and Name)
+            // with a public property.
+
+            var parameters = constructor.GetParameters();
+            var parametersWithPropertyInfo = new ConstructorParameter[parameters.Length];
+
+            for (var i = 0; i < parameters.Length; i++)
+            {
+                var key = new ParameterLookupKey(parameters[i].Name!, parameters[i].ParameterType);
+                if (!lookupTable.TryGetValue(key, out var property))
+                {
+                    throw new InvalidOperationException(
+                        $"The public parameterized constructor must contain only parameters that match the declared public properties for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.");
+                }
+
+                parametersWithPropertyInfo[i] = new ConstructorParameter(parameters[i], property);
+            }
+
+            return (constructor, parametersWithPropertyInfo);
+        }
+
+        return _constructorCache.GetOrAdd(type, Finder);
+    }
+
+    private static ConstructorInfo? GetConstructor(Type type)
+    {
+        if (type.IsAbstract)
+        {
+            throw new InvalidOperationException($"The abstract type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}' is not supported.");
+        }
+
+        var constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
+
+        // if only one constructor is declared
+        // we will use it to try match the properties
+        if (constructors.Length == 1)
+        {
+            return constructors[0];
+        }
+
+        // We will try to get the parameterless ctor
+        // as priority before visit the others
+        var parameterlessConstructor = constructors.SingleOrDefault(c => c.GetParameters().Length == 0);
+        if (parameterlessConstructor is not null)
+        {
+            return parameterlessConstructor;
+        }
+
+        // If a parameterized constructors is not found at this point
+        // we will use a default constructor that is always available
+        // for value types.
+        if (type.IsValueType)
+        {
+            return null;
+        }
+
+        // We don't have an attribute, similar to JsonConstructor, to
+        // disambiguate ctors, so, we will throw if more than one
+        // ctor is defined without a parameterless constructor.
+        // Eg.:
+        // public class X
+        // {
+        //   public X(int foo)
+        //   public X(int foo, int bar)
+        //   ...
+        // }
+        if (parameterlessConstructor is null && constructors.Length > 1)
+        {
+            throw new InvalidOperationException($"Only a single public parameterized constructor is allowed for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.");
+        }
+
+        throw new InvalidOperationException($"No public parameterless constructor found for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'.");
+    }
+
     private MethodInfo? GetStaticMethodFromHierarchy(Type type, string name, Type[] parameterTypes, Func<MethodInfo, bool> validateReturnType)
     {
         bool IsMatch(MethodInfo? method) => method is not null && !method.IsAbstract && validateReturnType(method);
@@ -535,4 +632,41 @@ internal sealed class ParameterBindingMethodCache
         static async ValueTask<object?> ConvertAwaited(ValueTask<Nullable<T>> typedValueTask) => await typedValueTask;
         return ConvertAwaited(typedValueTask);
     }
+
+    private sealed class ParameterLookupKey
+    {
+        public ParameterLookupKey(string name, Type type)
+        {
+            Name = name;
+            Type = type;
+        }
+
+        public string Name { get; }
+        public Type Type { get; }
+
+        public override int GetHashCode()
+        {
+            return StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
+        }
+
+        public override bool Equals([NotNullWhen(true)] object? obj)
+        {
+            Debug.Assert(obj is ParameterLookupKey);
+
+            var other = (ParameterLookupKey)obj;
+            return Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
+        }
+    }
+
+    internal sealed class ConstructorParameter
+    {
+        public ConstructorParameter(ParameterInfo parameter, PropertyInfo propertyInfo)
+        {
+            ParameterInfo = parameter;
+            PropertyInfo = propertyInfo;
+        }
+
+        public ParameterInfo ParameterInfo { get; }
+        public PropertyInfo PropertyInfo { get; }
+    }
 }

+ 177 - 0
src/Shared/PropertyAsParameterInfo.cs

@@ -0,0 +1,177 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.AspNetCore.Http;
+
+internal sealed class PropertyAsParameterInfo : ParameterInfo
+{
+    private readonly PropertyInfo _underlyingProperty;
+    private readonly ParameterInfo? _constructionParameterInfo;
+
+    private readonly NullabilityInfoContext _nullabilityContext;
+    private NullabilityInfo? _nullabilityInfo;
+
+    public PropertyAsParameterInfo(PropertyInfo propertyInfo, NullabilityInfoContext? nullabilityContext = null)
+    {
+        Debug.Assert(null != propertyInfo);
+
+        AttrsImpl = (ParameterAttributes)propertyInfo.Attributes;
+        NameImpl = propertyInfo.Name;
+        MemberImpl = propertyInfo;
+        ClassImpl = propertyInfo.PropertyType;
+
+        // It is not a real parameter in the delegate, so,
+        // not defining a real position.
+        PositionImpl = -1;
+
+        _nullabilityContext = nullabilityContext ?? new NullabilityInfoContext();
+        _underlyingProperty = propertyInfo;
+    }
+
+    public PropertyAsParameterInfo(PropertyInfo property, ParameterInfo parameterInfo, NullabilityInfoContext? nullabilityContext = null)
+        : this(property, nullabilityContext)
+    {
+        _constructionParameterInfo = parameterInfo;
+    }
+
+    public override bool HasDefaultValue
+        => _constructionParameterInfo is not null && _constructionParameterInfo.HasDefaultValue;
+    public override object? DefaultValue
+        => _constructionParameterInfo is not null ? _constructionParameterInfo.DefaultValue : null;
+    public override int MetadataToken => _underlyingProperty.MetadataToken;
+    public override object? RawDefaultValue
+        => _constructionParameterInfo is not null ? _constructionParameterInfo.RawDefaultValue : null;
+
+    /// <summary>
+    /// Unwraps all parameters that contains <see cref="AsParametersAttribute"/> and
+    /// creates a flat list merging the current parameters, not including the
+    /// parametres that contain a <see cref="AsParametersAttribute"/>, and all additional
+    /// parameters detected.
+    /// </summary>
+    /// <param name="parameters">List of parameters to be flattened.</param>
+    /// <param name="cache">An instance of the method cache class.</param>
+    /// <returns>Flat list of parameters.</returns>
+    [UnconditionalSuppressMessage("Trimmer", "IL2075", Justification = "PropertyAsParameterInfo.Flatten requires unreferenced code.")]
+    public static ReadOnlySpan<ParameterInfo> Flatten(ParameterInfo[] parameters, ParameterBindingMethodCache cache)
+    {
+        ArgumentNullException.ThrowIfNull(nameof(parameters));
+        ArgumentNullException.ThrowIfNull(nameof(cache));
+
+        if (parameters.Length == 0)
+        {
+            return Array.Empty<ParameterInfo>();
+        }
+
+        List<ParameterInfo>? flattenedParameters = null;
+        NullabilityInfoContext? nullabilityContext = null;
+
+        for (var i = 0; i < parameters.Length; i++)
+        {
+            if (parameters[i].CustomAttributes.Any(a => a.AttributeType == typeof(AsParametersAttribute)))
+            {
+                // Initialize the list with all parameter already processed
+                // to keep the same parameter ordering
+                flattenedParameters ??= new(parameters[0..i]);
+                nullabilityContext ??= new();
+
+                var (constructor, constructorParameters) = cache.FindConstructor(parameters[i].ParameterType);
+                if (constructor is not null && constructorParameters is { Length: > 0 })
+                {
+                    foreach (var constructorParameter in constructorParameters)
+                    {
+                        flattenedParameters.Add(
+                            new PropertyAsParameterInfo(
+                                constructorParameter.PropertyInfo,
+                                constructorParameter.ParameterInfo,
+                                nullabilityContext));
+                    }
+                }
+                else
+                {
+                    var properties = parameters[i].ParameterType.GetProperties();
+
+                    foreach (var property in properties)
+                    {
+                        if (property.CanWrite)
+                        {
+                            flattenedParameters.Add(new PropertyAsParameterInfo(property, nullabilityContext));
+                        }
+                    }
+                }
+            }
+            else if (flattenedParameters is not null)
+            {
+                flattenedParameters.Add(parameters[i]);
+            }
+        }
+
+        return flattenedParameters is not null ? CollectionsMarshal.AsSpan(flattenedParameters) : parameters.AsSpan();
+    }
+
+    public override object[] GetCustomAttributes(Type attributeType, bool inherit)
+    {
+        var attributes = _constructionParameterInfo?.GetCustomAttributes(attributeType, inherit);
+
+        if (attributes == null || attributes is { Length: 0 })
+        {
+            attributes = _underlyingProperty.GetCustomAttributes(attributeType, inherit);
+        }
+
+        return attributes;
+    }
+
+    public override object[] GetCustomAttributes(bool inherit)
+    {
+        var constructorAttributes = _constructionParameterInfo?.GetCustomAttributes(inherit);
+
+        if (constructorAttributes == null || constructorAttributes is { Length: 0 })
+        {
+            return _underlyingProperty.GetCustomAttributes(inherit);
+        }
+
+        var propertyAttributes = _underlyingProperty.GetCustomAttributes(inherit);
+
+        // Since the constructors attributes should take priority we will add them first,
+        // as we usually call it as First() or FirstOrDefault() in the argument creation
+        var mergedAttributes = new object[constructorAttributes.Length + propertyAttributes.Length];
+        Array.Copy(constructorAttributes, mergedAttributes, constructorAttributes.Length);
+        Array.Copy(propertyAttributes, 0, mergedAttributes, constructorAttributes.Length, propertyAttributes.Length);
+
+        return mergedAttributes;
+    }
+
+    public override IList<CustomAttributeData> GetCustomAttributesData()
+    {
+        var attributes = new List<CustomAttributeData>(
+            _constructionParameterInfo?.GetCustomAttributesData() ?? Array.Empty<CustomAttributeData>());
+        attributes.AddRange(_underlyingProperty.GetCustomAttributesData());
+
+        return attributes.AsReadOnly();
+    }
+
+    public override Type[] GetOptionalCustomModifiers()
+        => _underlyingProperty.GetOptionalCustomModifiers();
+
+    public override Type[] GetRequiredCustomModifiers()
+        => _underlyingProperty.GetRequiredCustomModifiers();
+
+    public override bool IsDefined(Type attributeType, bool inherit)
+    {
+        return (_constructionParameterInfo is not null && _constructionParameterInfo.IsDefined(attributeType, inherit)) ||
+            _underlyingProperty.IsDefined(attributeType, inherit);
+    }
+
+    public new bool IsOptional => HasDefaultValue || NullabilityInfo.ReadState != NullabilityState.NotNull;
+
+    public NullabilityInfo NullabilityInfo
+        => _nullabilityInfo ??= _constructionParameterInfo is not null ?
+        _nullabilityContext.Create(_constructionParameterInfo) :
+        _nullabilityContext.Create(_underlyingProperty);
+}