Просмотр исходного кода

Add complex form binding support to RDF (#48790)

* Add complex form binding support to RDF

* Address feedback from peer review
Safia Abdalla 2 лет назад
Родитель
Сommit
c53f18a72d
20 измененных файлов с 353 добавлено и 7 удалено
  1. 5 0
      src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs
  2. 6 0
      src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs
  3. 9 0
      src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs
  4. 4 0
      src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs
  5. 5 0
      src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs
  6. 5 1
      src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs
  7. 6 0
      src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs
  8. 5 0
      src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs
  9. 5 0
      src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs
  10. 5 0
      src/Components/Endpoints/src/Binding/Factories/NullableConverterFactory.cs
  11. 5 0
      src/Components/Endpoints/src/Binding/Factories/ParsableConverterFactory.cs
  12. 10 0
      src/Components/Endpoints/src/Binding/FormBindingHelpers.cs
  13. 4 1
      src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs
  14. 6 0
      src/Components/Endpoints/src/Binding/IFormDataConverterFactory.cs
  15. 1 0
      src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj
  16. 5 0
      src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj
  17. 56 2
      src/Http/Http.Extensions/src/RequestDelegateFactory.cs
  18. 207 0
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.ComplexFormBinding.cs
  19. 1 1
      src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs
  20. 3 2
      src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs

+ 5 - 0
src/Components/Endpoints/src/Binding/Factories/CollectionConverterFactory.cs

@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.Extensions.Internal;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
@@ -9,6 +10,8 @@ internal class CollectionConverterFactory : IFormDataConverterFactory
 {
     public static readonly CollectionConverterFactory Instance = new();
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public bool CanConvert(Type type, FormDataMapperOptions options)
     {
         var enumerable = ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IEnumerable<>));
@@ -28,6 +31,8 @@ internal class CollectionConverterFactory : IFormDataConverterFactory
         return factory.CanConvert(type, options);
     }
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
     {
         ArgumentNullException.ThrowIfNull(type);

+ 6 - 0
src/Components/Endpoints/src/Binding/Factories/Collections/ConcreteTypeCollectionConverterFactory.cs

@@ -1,6 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
 internal class ConcreteTypeCollectionConverterFactory<TCollection, TElement>
@@ -9,8 +11,12 @@ internal class ConcreteTypeCollectionConverterFactory<TCollection, TElement>
     public static readonly ConcreteTypeCollectionConverterFactory<TCollection, TElement> Instance =
         new();
 
+    [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This derived implementation doesn't require unreferenced code like other implementations of the interface.")]
+    [UnconditionalSuppressMessage("AOT", "IL3051", Justification = "This derived implementation doesn't use dynamic code like other implementations of the interface.")]
     public bool CanConvert(Type _, FormDataMapperOptions options) => true;
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type _, FormDataMapperOptions options)
     {
         // Resolve the element type converter

+ 9 - 0
src/Components/Endpoints/src/Binding/Factories/Collections/TypedCollectionConverterFactory.cs

@@ -4,18 +4,25 @@
 using System.Collections.Concurrent;
 using System.Collections.Immutable;
 using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
 internal abstract class TypedCollectionConverterFactory : IFormDataConverterFactory
 {
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public abstract bool CanConvert(Type type, FormDataMapperOptions options);
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public abstract FormDataConverter CreateConverter(Type type, FormDataMapperOptions options);
 }
 
 internal sealed class TypedCollectionConverterFactory<TCollection, TElement> : TypedCollectionConverterFactory
 {
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public override bool CanConvert(Type _, FormDataMapperOptions options)
     {
         // Resolve the element type converter
@@ -101,6 +108,8 @@ internal sealed class TypedCollectionConverterFactory<TCollection, TElement> : T
     //   the Queue directly as the buffer (queues don't implement ICollection<T>, so the adapter uses Push instead),
     //   or for ImmutableXXX<T> we either use ImmuttableXXX.CreateBuilder<T> to create a builder we use as a buffer,
     //   or collect the collection into an array buffer and call CreateRange to build the final collection.
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public override FormDataConverter CreateConverter(Type _, FormDataMapperOptions options)
     {
         // Resolve the element type converter

+ 4 - 0
src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactory.cs

@@ -1,9 +1,13 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
 internal abstract class ComplexTypeExpressionConverterFactory
 {
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     internal abstract FormDataConverter CreateConverter(Type type, FormDataMapperOptions options);
 }

+ 5 - 0
src/Components/Endpoints/src/Binding/Factories/ComplexType/ComplexTypeExpressionConverterFactoryOfT.cs

@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
 using System.Linq.Expressions;
 using Microsoft.Extensions.Internal;
 
@@ -8,12 +9,16 @@ namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
 internal sealed class ComplexTypeExpressionConverterFactory<T> : ComplexTypeExpressionConverterFactory
 {
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     internal override CompiledComplexTypeConverter<T> CreateConverter(Type type, FormDataMapperOptions options)
     {
         var body = CreateConverterBody(type, options);
         return new CompiledComplexTypeConverter<T>(body);
     }
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     private CompiledComplexTypeConverter<T>.ConverterDelegate CreateConverterBody(Type type, FormDataMapperOptions options)
     {
         var properties = PropertyHelper.GetVisibleProperties(type);

+ 5 - 1
src/Components/Endpoints/src/Binding/Factories/ComplexTypeConverterFactory.cs

@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.Extensions.Internal;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
@@ -11,6 +12,8 @@ internal class ComplexTypeConverterFactory : IFormDataConverterFactory
 {
     internal static readonly ComplexTypeConverterFactory Instance = new();
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public bool CanConvert(Type type, FormDataMapperOptions options)
     {
         if (type.GetConstructor(Type.EmptyTypes) == null && !type.IsValueType)
@@ -105,7 +108,8 @@ internal class ComplexTypeConverterFactory : IFormDataConverterFactory
     //         return converterFunc(ref reader, type, options, out result, out found);
     //     }
     // }
-
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
     {
         if (Activator.CreateInstance(typeof(ComplexTypeExpressionConverterFactory<>).MakeGenericType(type))

+ 6 - 0
src/Components/Endpoints/src/Binding/Factories/Dictionary/ConcreteTypeDictionaryConverterFactory.cs

@@ -1,6 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
 internal sealed class ConcreteTypeDictionaryConverterFactory<TDictionary, TKey, TValue> : IFormDataConverterFactory
@@ -8,8 +10,12 @@ internal sealed class ConcreteTypeDictionaryConverterFactory<TDictionary, TKey,
 {
     public static readonly ConcreteTypeDictionaryConverterFactory<TDictionary, TKey, TValue> Instance = new();
 
+    [UnconditionalSuppressMessage("Trimming", "IL2046", Justification = "This derived implementation doesn't require unreferenced code like other implementations of the interface.")]
+    [UnconditionalSuppressMessage("AOT", "IL3051", Justification = "This derived implementation doesn't use dynamic code like other implementations of the interface.")]
     public bool CanConvert(Type type, FormDataMapperOptions options) => true;
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
     {
         // Resolve the element type converter

+ 5 - 0
src/Components/Endpoints/src/Binding/Factories/Dictionary/TypedDictionaryConverterFactory.cs

@@ -4,12 +4,15 @@
 using System.Collections.Concurrent;
 using System.Collections.Immutable;
 using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
 internal sealed class TypedDictionaryConverterFactory<TDictionaryType, TKey, TValue> : IFormDataConverterFactory
     where TKey : IParsable<TKey>
 {
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public bool CanConvert(Type type, FormDataMapperOptions options)
     {
         // Resolve the value type converter
@@ -70,6 +73,8 @@ internal sealed class TypedDictionaryConverterFactory<TDictionaryType, TKey, TVa
         return false;
     }
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
     {
         // Resolve the value type converter

+ 5 - 0
src/Components/Endpoints/src/Binding/Factories/DictionaryConverterFactory.cs

@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.Extensions.Internal;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
@@ -9,6 +10,8 @@ internal class DictionaryConverterFactory : IFormDataConverterFactory
 {
     internal static readonly DictionaryConverterFactory Instance = new();
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public bool CanConvert(Type type, FormDataMapperOptions options)
     {
         // Type must implement IDictionary<TKey, TValue> IReadOnlyDictionary<TKey, TValue>
@@ -58,6 +61,8 @@ internal class DictionaryConverterFactory : IFormDataConverterFactory
         return factory.CanConvert(type, options);
     }
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
     {
         // Type must implement IDictionary<TKey, TValue> IReadOnlyDictionary<TKey, TValue>

+ 5 - 0
src/Components/Endpoints/src/Binding/Factories/NullableConverterFactory.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
@@ -9,12 +10,16 @@ internal sealed class NullableConverterFactory : IFormDataConverterFactory
 {
     public static readonly NullableConverterFactory Instance = new();
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public bool CanConvert(Type type, FormDataMapperOptions options)
     {
         var underlyingType = Nullable.GetUnderlyingType(type);
         return underlyingType != null && options.ResolveConverter(underlyingType) != null;
     }
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
     {
         var underlyingType = Nullable.GetUnderlyingType(type);

+ 5 - 0
src/Components/Endpoints/src/Binding/Factories/ParsableConverterFactory.cs

@@ -1,6 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
 using Microsoft.Extensions.Internal;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
@@ -9,11 +10,15 @@ internal sealed class ParsableConverterFactory : IFormDataConverterFactory
 {
     public static readonly ParsableConverterFactory Instance = new();
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public bool CanConvert(Type type, FormDataMapperOptions options)
     {
         return ClosedGenericMatcher.ExtractGenericInterface(type, typeof(IParsable<>)) is not null;
     }
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options)
     {
         return Activator.CreateInstance(typeof(ParsableConverter<>).MakeGenericType(type)) as FormDataConverter ??

+ 10 - 0
src/Components/Endpoints/src/Binding/FormBindingHelpers.cs

@@ -0,0 +1,10 @@
+// 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.Components.Endpoints.Binding;
+
+internal static class FormBindingHelpers
+{
+    public const string RequiresUnreferencedCodeMessage = "Form binding is not compatible with trimming, as it requires dynamic access to code that is not referenced statically.";
+    public const string RequiresDynamicCodeMessage = "Form binding may require dynamic code generation.";
+}

+ 4 - 1
src/Components/Endpoints/src/Binding/FormDataMapperOptions.cs

@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
 
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
@@ -10,6 +11,8 @@ internal sealed class FormDataMapperOptions
     private readonly ConcurrentDictionary<Type, FormDataConverter> _converters = new();
     private readonly List<Func<Type, FormDataMapperOptions, FormDataConverter?>> _factories = new();
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataMapperOptions()
     {
         _converters = new(WellKnownConverters.Converters);
@@ -27,7 +30,7 @@ internal sealed class FormDataMapperOptions
     // Binding to collection using hashes, where the payload can be crafted to force the worst case on insertion
     // which is O(n).
     internal int MaxCollectionSize = 100;
-  
+
     internal bool HasConverter(Type valueType) => _converters.ContainsKey(valueType);
 
     internal bool IsSingleValueConverter(Type type)

+ 6 - 0
src/Components/Endpoints/src/Binding/IFormDataConverterFactory.cs

@@ -1,11 +1,17 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System.Diagnostics.CodeAnalysis;
+
 namespace Microsoft.AspNetCore.Components.Endpoints.Binding;
 
 internal interface IFormDataConverterFactory
 {
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public bool CanConvert(Type type, FormDataMapperOptions options);
 
+    [RequiresDynamicCode(FormBindingHelpers.RequiresDynamicCodeMessage)]
+    [RequiresUnreferencedCode(FormBindingHelpers.RequiresUnreferencedCodeMessage)]
     public FormDataConverter CreateConverter(Type type, FormDataMapperOptions options);
 }

+ 1 - 0
src/Features/JsonPatch/src/Microsoft.AspNetCore.JsonPatch.csproj

@@ -18,6 +18,7 @@
 
   <ItemGroup Condition="'$(TargetFramework)' != '$(DefaultNetCoreTargetFramework)'">
     <Compile Include="$(SharedSourceRoot)Nullable\NullableAttributes.cs" LinkBase="Shared" />
+    <Compile Include="$(SharedSourceRoot)TrimmingAttributes.cs" LinkBase="Shared" />
   </ItemGroup>
 
   <ItemGroup>

+ 5 - 0
src/Http/Http.Extensions/src/Microsoft.AspNetCore.Http.Extensions.csproj

@@ -26,6 +26,11 @@
     <Compile Include="$(SharedSourceRoot)Json\JsonSerializerExtensions.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)RouteHandlers\ExecuteHandlerHelper.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)Debugger\DebuggerHelpers.cs" LinkBase="Shared" />
+    <Compile Include="$(RepoRoot)src\Components\Endpoints\src\Binding\**\*.cs" LinkBase="SharedFormBinding" />
+    <Compile Include="$(RepoRoot)src\Shared\ClosedGenericMatcher\ClosedGenericMatcher.cs" LinkBase="SharedFormBinding" />
+    <!-- Exclude Blazor-specific form binding suppliers. -->
+    <Compile Remove="$(RepoRoot)src\Components\Endpoints\src\Binding\DefaultFormValuesSupplier.cs" LinkBase="SharedFormBinding" />
+    <Compile Remove="$(RepoRoot)src\Components\Endpoints\src\Binding\HttpContextFormDataProvider.cs" LinkBase="SharedFormBinding" />
   </ItemGroup>
 
   <ItemGroup>

+ 56 - 2
src/Http/Http.Extensions/src/RequestDelegateFactory.cs

@@ -1,6 +1,7 @@
 // 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.Globalization;
@@ -14,6 +15,7 @@ using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization.Metadata;
 using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Components.Endpoints.Binding;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Http.HttpResults;
 using Microsoft.AspNetCore.Http.Json;
@@ -36,6 +38,7 @@ namespace Microsoft.AspNetCore.Http;
 public static partial class RequestDelegateFactory
 {
     private static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new();
+    private static readonly FormDataMapperOptions FormDataMapperOptions = new();
 
     private static readonly MethodInfo ExecuteTaskWithEmptyResultMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskWithEmptyResult), BindingFlags.NonPublic | BindingFlags.Static)!;
     private static readonly MethodInfo ExecuteValueTaskWithEmptyResultMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskWithEmptyResult), BindingFlags.NonPublic | BindingFlags.Static)!;
@@ -113,6 +116,10 @@ public static partial class RequestDelegateFactory
     private static readonly MemberExpression FilterContextHttpContextStatusCodeExpr = Expression.Property(FilterContextHttpContextResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!);
     private static readonly ParameterExpression InvokedFilterContextExpr = Expression.Parameter(typeof(EndpointFilterInvocationContext), "filterContext");
 
+    private static readonly ConstructorInfo FormDataReaderConstructor = typeof(FormDataReader).GetConstructor(new[] { typeof(IReadOnlyDictionary<string, StringValues>), typeof(CultureInfo) })!;
+    private static readonly MethodInfo FormToReadOnlyDictionaryMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ToReadOnlyDictionary), BindingFlags.Static | BindingFlags.NonPublic, new[] { typeof(IFormCollection) })!;
+    private static readonly MethodInfo FormDataMapperMapMethod = typeof(FormDataMapper).GetMethod(nameof(FormDataMapper.Map))!;
+
     private static readonly string[] DefaultAcceptsAndProducesContentType = new[] { JsonConstants.JsonContentType };
     private static readonly string[] FormFileContentType = new[] { "multipart/form-data" };
     private static readonly string[] FormContentType = new[] { "multipart/form-data", "application/x-www-form-urlencoded" };
@@ -730,8 +737,19 @@ public static partial class RequestDelegateFactory
                 }
                 return BindParameterFromFormCollection(parameter, factoryContext);
             }
-
-            return BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext);
+            // Continue to use the simple binding support that exists in RDF/RDG for currently
+            // supported scenarios to maintain compatible semantics between versions of RDG. 
+            // For complex types, leverage the shared form binding infrastructure. For example,
+            // shared form binding does not currently only supports types that implement IParsable
+            // while RDF's binding implementation supports all TryParse implementations.
+            var useSimpleBinding = parameter.ParameterType == typeof(string) ||
+                parameter.ParameterType == typeof(StringValues) ||
+                parameter.ParameterType == typeof(StringValues?) ||
+                ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType) ||
+                (parameter.ParameterType.IsArray && ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType.GetElementType()!));
+            return useSimpleBinding
+                ? BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext)
+                : BindComplexParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext);
         }
         else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
         {
@@ -1943,6 +1961,42 @@ public static partial class RequestDelegateFactory
             "form");
     }
 
+    private static Expression BindComplexParameterFromFormItem(
+        ParameterInfo parameter,
+        string key,
+        RequestDelegateFactoryContext factoryContext)
+    {
+        factoryContext.FirstFormRequestBodyParameter ??= parameter;
+        factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute);
+        factoryContext.ReadForm = true;
+
+        // var name_local;
+        // var name_reader;
+        var formArgument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local");
+        var formReader = Expression.Variable(typeof(FormDataReader), $"{parameter.Name}_reader");
+
+        // name_reader = new FormDataReader(context.Request.Form.ToReadOnlyDictionary()), CultureInfo.InvariantCulture);
+        var initializeReaderExpr = Expression.Assign(
+            formReader,
+            Expression.New(FormDataReaderConstructor,
+                Expression.Call(FormToReadOnlyDictionaryMethod, FormExpr),
+                Expression.Constant(CultureInfo.InvariantCulture)));
+        // FormDataMapper.Map<string>(name_reader, FormDataMapperOptions);
+        var invokeMapMethodExpr = Expression.Call(
+            FormDataMapperMapMethod.MakeGenericMethod(parameter.ParameterType),
+            formReader,
+            Expression.Constant(FormDataMapperOptions));
+
+        return Expression.Block(
+            new[] { formArgument, formReader },
+            initializeReaderExpr,
+            Expression.Assign(formArgument, invokeMapMethodExpr)
+        );
+    }
+
+    private static IReadOnlyDictionary<string, StringValues> ToReadOnlyDictionary(IFormCollection form)
+        => new ReadOnlyDictionary<string, StringValues>(form.ToDictionary());
+
     private static Expression BindParameterFromFormFiles(
         ParameterInfo parameter,
         RequestDelegateFactoryContext factoryContext)

+ 207 - 0
src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.ComplexFormBinding.cs

@@ -0,0 +1,207 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Http.Generators.Tests;
+
+public partial class RuntimeCreationTests : RequestDelegateCreationTests
+{
+    [Fact]
+    public async Task SupportsBindingComplexTypeFromForm_UrlEncoded()
+    {
+        var source = """
+app.MapPost("/", ([FromForm] Todo todo) => Results.Ok(todo));
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+        var httpContext = CreateHttpContext();
+
+        var content = new FormUrlEncodedContent(new Dictionary<string, string> { ["Id"] = "1", ["Name"] = "Write tests", ["IsComplete"] = "true" });
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        await VerifyResponseJsonBodyAsync<Todo>(httpContext, (todo) =>
+        {
+            Assert.Equal(1, todo.Id);
+            Assert.Equal("Write tests", todo.Name);
+            Assert.True(todo.IsComplete);
+        });
+    }
+
+    [Fact]
+    public async Task SupportsBindingComplexTypeFromForm_Multipart()
+    {
+        var source = """
+app.MapPost("/", ([FromForm] Todo todo) => Results.Ok(todo));
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+        var httpContext = CreateHttpContext();
+
+        var content = new MultipartFormDataContent("some-boundary");
+        content.Add(new StringContent("1"), "Id");
+        content.Add(new StringContent("Write tests"), "Name");
+        content.Add(new StringContent("true"), "IsComplete");
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        await VerifyResponseJsonBodyAsync<Todo>(httpContext, (todo) =>
+        {
+            Assert.Equal(1, todo.Id);
+            Assert.Equal("Write tests", todo.Name);
+            Assert.True(todo.IsComplete);
+        });
+    }
+
+    [Fact]
+    public async Task SupportsBindingDictionaryFromForm_UrlEncoded()
+    {
+        var source = """
+app.MapPost("/", ([FromForm] Dictionary<string, bool> elements) => Results.Ok(elements));
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+        var httpContext = CreateHttpContext();
+
+        var content = new FormUrlEncodedContent(new Dictionary<string, string> { ["[foo]"] = "true", ["[bar]"] = "false", ["[baz]"] = "true" });
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        await VerifyResponseJsonBodyAsync<Dictionary<string, bool>>(httpContext, (elements) =>
+        {
+            Assert.Equal(3, elements.Count);
+            Assert.True(elements["foo"]);
+            Assert.False(elements["bar"]);
+            Assert.True(elements["baz"]);
+        });
+    }
+
+    [Fact]
+    public async Task SupportsBindingDictionaryFromForm_Multipart()
+    {
+        var source = """
+app.MapPost("/", ([FromForm] Dictionary<string, bool> elements) => Results.Ok(elements));
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+        var httpContext = CreateHttpContext();
+
+        var content = new MultipartFormDataContent("some-boundary");
+        content.Add(new StringContent("true"), "[foo]");
+        content.Add(new StringContent("false"), "[bar]");
+        content.Add(new StringContent("true"), "[baz]");
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        await VerifyResponseJsonBodyAsync<Dictionary<string, bool>>(httpContext, (elements) =>
+        {
+            Assert.Equal(3, elements.Count);
+            Assert.True(elements["foo"]);
+            Assert.False(elements["bar"]);
+            Assert.True(elements["baz"]);
+        });
+    }
+
+    [Fact]
+    public async Task SupportsBindingListFromForm_UrlEncoded()
+    {
+        var source = """
+app.MapPost("/", ([FromForm] List<int> elements) => Results.Ok(elements));
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+        var httpContext = CreateHttpContext();
+
+        var content = new FormUrlEncodedContent(new Dictionary<string, string> { ["[0]"] = "1", ["[1]"] = "3", ["[2]"] = "5" });
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "application/x-www-form-urlencoded";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        await VerifyResponseJsonBodyAsync<List<int>>(httpContext, (elements) =>
+        {
+            Assert.Equal(3, elements.Count);
+            Assert.Equal(1, elements[0]);
+            Assert.Equal(3, elements[1]);
+            Assert.Equal(5, elements[2]);
+        });
+    }
+
+    [Fact]
+    public async Task SupportsBindingListFromForm_Multipart()
+    {
+        var source = """
+app.MapPost("/", ([FromForm] List<int> elements) => Results.Ok(elements));
+""";
+        var (_, compilation) = await RunGeneratorAsync(source);
+        var endpoint = GetEndpointFromCompilation(compilation);
+        var httpContext = CreateHttpContext();
+
+        var content = new MultipartFormDataContent("some-boundary");
+        content.Add(new StringContent("1"), "[0]");
+        content.Add(new StringContent("3"), "[1]");
+        content.Add(new StringContent("5"), "[2]");
+
+        var stream = new MemoryStream();
+        await content.CopyToAsync(stream);
+
+        stream.Seek(0, SeekOrigin.Begin);
+
+        httpContext.Request.Body = stream;
+        httpContext.Request.Headers["Content-Type"] = "multipart/form-data;boundary=some-boundary";
+        httpContext.Features.Set<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(true));
+
+        await endpoint.RequestDelegate(httpContext);
+
+        await VerifyResponseJsonBodyAsync<List<int>>(httpContext, (elements) =>
+        {
+            Assert.Equal(3, elements.Count);
+            Assert.Equal(1, elements[0]);
+            Assert.Equal(3, elements[1]);
+            Assert.Equal(5, elements[2]);
+        });
+    }
+}

+ 1 - 1
src/Http/Http.Extensions/test/RequestDelegateGenerator/RuntimeCreationTests.cs

@@ -8,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 namespace Microsoft.AspNetCore.Http.Generators.Tests;
 
-public class RuntimeCreationTests : RequestDelegateCreationTests
+public partial class RuntimeCreationTests : RequestDelegateCreationTests
 {
     protected override bool IsGeneratorEnabled { get; } = false;
 

+ 3 - 2
src/Shared/ClosedGenericMatcher/ClosedGenericMatcher.cs

@@ -4,6 +4,7 @@
 #nullable enable
 
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using Microsoft.AspNetCore.Shared;
 
@@ -30,7 +31,7 @@ internal static class ClosedGenericMatcher
     /// <c>typeof(KeyValuePair{,})</c>, and <paramref name="queryType"/> is
     /// <c>typeof(KeyValuePair{string, object})</c>.
     /// </remarks>
-    public static Type? ExtractGenericInterface(Type queryType, Type interfaceType)
+    public static Type? ExtractGenericInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]  Type queryType, Type interfaceType)
     {
 #if !NET8_0_OR_GREATER
         ArgumentNullThrowHelper.ThrowIfNull(queryType);
@@ -62,7 +63,7 @@ internal static class ClosedGenericMatcher
             candidate.GetGenericTypeDefinition() == interfaceType;
     }
 
-    private static Type? GetGenericInstantiation(Type queryType, Type interfaceType)
+    private static Type? GetGenericInstantiation([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type queryType, Type interfaceType)
     {
         Type? bestMatch = null;
         var interfaces = queryType.GetInterfaces();