From f1ff78ae4670d41bb60a41f028beafe8381938eb Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 4 Apr 2022 11:32:07 -0700 Subject: [PATCH 01/13] Added IBindableFromHttpContext interface --- .../src/IBindableFromHttpContextOfT.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs diff --git a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs new file mode 100644 index 000000000000..59ed79711c10 --- /dev/null +++ b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs @@ -0,0 +1,23 @@ +// 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; + +/// +/// Defines a mechanism for creating an instance of a type from an when binding parameters for an endpoint +/// route handler delegate. +/// +/// The type that implements this interface. +public interface IBindableFromHttpContext + where TSelf : IBindableFromHttpContext +{ + /// + /// Creates an instance of from the . + /// + /// The for the current request. + /// The for the parameter of the route handler delegate the returned instance will populate. + /// The instance of the type. + static abstract ValueTask BindAsync(HttpContext context, ParameterInfo parameter); +} From ebf426ad3cc542435de6fb405d69594f6dfc986c Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 7 Apr 2022 16:52:55 -0700 Subject: [PATCH 02/13] Discover BindAsync via IBindableFromHttpContext interface --- .../src/IBindableFromHttpContextOfT.cs | 17 ++++++----- src/Http/samples/MinimalSample/Program.cs | 14 ++++++++++ src/Shared/ParameterBindingMethodCache.cs | 28 +++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs index 59ed79711c10..c5c8061d148e 100644 --- a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs +++ b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs @@ -10,14 +10,13 @@ namespace Microsoft.AspNetCore.Http; /// route handler delegate. /// /// The type that implements this interface. -public interface IBindableFromHttpContext - where TSelf : IBindableFromHttpContext +public interface IBindableFromHttpContext where TSelf : IBindableFromHttpContext { - /// - /// Creates an instance of from the . - /// - /// The for the current request. - /// The for the parameter of the route handler delegate the returned instance will populate. - /// The instance of the type. - static abstract ValueTask BindAsync(HttpContext context, ParameterInfo parameter); + /// + /// Creates an instance of from the . + /// + /// The for the current request. + /// The for the parameter of the route handler delegate the returned instance will populate. + /// The instance of . + static abstract ValueTask BindAsync(HttpContext context, ParameterInfo parameter); } diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs index a15f67986088..27923bed88b3 100644 --- a/src/Http/samples/MinimalSample/Program.cs +++ b/src/Http/samples/MinimalSample/Program.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http.HttpResults; +using System.Reflection; using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); @@ -63,6 +64,19 @@ }); +app.MapPost("/todos", (TodoBindable todo) => todo); + app.Run(); internal record Todo(int Id, string Title); +public class TodoBindable : IBindableFromHttpContext +{ + public int Id { get; set; } + public string Title { get; set; } + public bool IsComplete { get; set; } + + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new TodoBindable { Id = 1, Title = "I was bound from IBindableFromHttpContext.BindAsync!" }); + } +} diff --git a/src/Shared/ParameterBindingMethodCache.cs b/src/Shared/ParameterBindingMethodCache.cs index 99418b00261f..b7ed2391e9d1 100644 --- a/src/Shared/ParameterBindingMethodCache.cs +++ b/src/Shared/ParameterBindingMethodCache.cs @@ -184,7 +184,35 @@ static bool ValidateReturnType(MethodInfo methodInfo) { (Func?, int) Finder(Type nonNullableParameterType) { + // Check if parameter is bindable via static abstract method on IBindableFromHttpContext + + var isBindableViaInterface = true; + try + { + var bindableType = typeof(IBindableFromHttpContext<>).MakeGenericType(nonNullableParameterType); + } + catch (ArgumentException) + { + isBindableViaInterface = false; + } + + if (isBindableViaInterface) + { + var staticMethodInfo = nonNullableParameterType.GetMethod("BindAsync", BindingFlags.Public | BindingFlags.Static)!; + return ((parameter) => + { + // parameter is being intentionally shadowed. We never want to use the outer ParameterInfo inside + // this Func because the ParameterInfo varies after it's been cached for a given parameter type. + MethodCallExpression typedCall = Expression.Call(staticMethodInfo, HttpContextExpr, Expression.Constant(parameter)); + return Expression.Call(GetGenericConvertValueTask(nonNullableParameterType), typedCall); + }, 2); + + [UnconditionalSuppressMessage("Trimmer", "IL2060", Justification = "Linker workaround. The type is annotated with RequiresUnreferencedCode")] + static MethodInfo GetGenericConvertValueTask(Type nonNullableParameterType) => ConvertValueTaskMethod.MakeGenericMethod(nonNullableParameterType); + } + var hasParameterInfo = true; + // There should only be one BindAsync method with these parameters since C# does not allow overloading on return type. var methodInfo = GetStaticMethodFromHierarchy(nonNullableParameterType, "BindAsync", new[] { typeof(HttpContext), typeof(ParameterInfo) }, ValidateReturnType); if (methodInfo is null) From 676162592e356f05cf2d9dfaa90b9991891b8b7f Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 7 Apr 2022 17:35:32 -0700 Subject: [PATCH 03/13] Refactoring --- src/Shared/ParameterBindingMethodCache.cs | 63 ++++++++++++----------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/Shared/ParameterBindingMethodCache.cs b/src/Shared/ParameterBindingMethodCache.cs index b7ed2391e9d1..43ac79509890 100644 --- a/src/Shared/ParameterBindingMethodCache.cs +++ b/src/Shared/ParameterBindingMethodCache.cs @@ -184,41 +184,19 @@ static bool ValidateReturnType(MethodInfo methodInfo) { (Func?, int) Finder(Type nonNullableParameterType) { - // Check if parameter is bindable via static abstract method on IBindableFromHttpContext - - var isBindableViaInterface = true; - try - { - var bindableType = typeof(IBindableFromHttpContext<>).MakeGenericType(nonNullableParameterType); - } - catch (ArgumentException) - { - isBindableViaInterface = false; - } - - if (isBindableViaInterface) - { - var staticMethodInfo = nonNullableParameterType.GetMethod("BindAsync", BindingFlags.Public | BindingFlags.Static)!; - return ((parameter) => - { - // parameter is being intentionally shadowed. We never want to use the outer ParameterInfo inside - // this Func because the ParameterInfo varies after it's been cached for a given parameter type. - MethodCallExpression typedCall = Expression.Call(staticMethodInfo, HttpContextExpr, Expression.Constant(parameter)); - return Expression.Call(GetGenericConvertValueTask(nonNullableParameterType), typedCall); - }, 2); - - [UnconditionalSuppressMessage("Trimmer", "IL2060", Justification = "Linker workaround. The type is annotated with RequiresUnreferencedCode")] - static MethodInfo GetGenericConvertValueTask(Type nonNullableParameterType) => ConvertValueTaskMethod.MakeGenericMethod(nonNullableParameterType); - } - var hasParameterInfo = true; + var methodInfo = GetIBindableFromHttpContextMethod(nonNullableParameterType); - // There should only be one BindAsync method with these parameters since C# does not allow overloading on return type. - var methodInfo = GetStaticMethodFromHierarchy(nonNullableParameterType, "BindAsync", new[] { typeof(HttpContext), typeof(ParameterInfo) }, ValidateReturnType); if (methodInfo is null) { - hasParameterInfo = false; - methodInfo = GetStaticMethodFromHierarchy(nonNullableParameterType, "BindAsync", new[] { typeof(HttpContext) }, ValidateReturnType); + // There should only be one BindAsync method with these parameters since C# does not allow overloading on return type. + methodInfo = GetStaticMethodFromHierarchy(nonNullableParameterType, "BindAsync", new[] { typeof(HttpContext), typeof(ParameterInfo) }, ValidateReturnType); + + if (methodInfo is null) + { + hasParameterInfo = false; + methodInfo = GetStaticMethodFromHierarchy(nonNullableParameterType, "BindAsync", new[] { typeof(HttpContext) }, ValidateReturnType); + } } // We're looking for a method with the following signatures: @@ -401,6 +379,29 @@ static bool ValidateReturnType(MethodInfo methodInfo) throw new InvalidOperationException($"No public parameterless constructor found for type '{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}'."); } + private static MethodInfo? GetIBindableFromHttpContextMethod(Type type) + { + // Check if parameter is bindable via static abstract method on IBindableFromHttpContext + // JonSkeet himself said this is the way (https://stackoverflow.com/a/4864565/405892) + // but if we find another less exceptiony way we should probably do that instead + var isBindableViaInterface = true; + try + { + var _ = typeof(IBindableFromHttpContext<>).MakeGenericType(type); + } + catch (ArgumentException) + { + isBindableViaInterface = false; + } + + if (isBindableViaInterface) + { + return type.GetMethod("BindAsync", BindingFlags.Public | BindingFlags.Static)!; + } + + return null; + } + private MethodInfo? GetStaticMethodFromHierarchy(Type type, string name, Type[] parameterTypes, Func validateReturnType) { bool IsMatch(MethodInfo? method) => method is not null && !method.IsAbstract && validateReturnType(method); From 13868fcb5b37c8fe5a75232c9e283d074740d566 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 8 Apr 2022 11:22:53 -0700 Subject: [PATCH 04/13] Refactor IBindableFromHttpContext discovery & add tests --- .../test/ParameterBindingMethodCacheTests.cs | 35 ++++++++++++++++++- src/Shared/ParameterBindingMethodCache.cs | 23 ++++++------ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index 6a1085ca2c4f..32e14e633f99 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -320,6 +320,13 @@ public void HasBindAsyncMethod_ReturnsTrueForNullableReturningBindAsyncStructMet Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); } + [Fact] + public void HasBindAsyncMethod_ReturnsTrueForClassImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterface arg) => BindAsyncFromStaticAbstractInterfaceMethod(arg)); + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } + [Fact] public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNullableType() { @@ -327,6 +334,24 @@ public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNul Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); } + [Fact] + public async Task FindBindAsyncMethod_FindsForClassImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterface arg) => BindAsyncFromStaticAbstractInterfaceMethod(arg)); + var cache = new ParameterBindingMethodCache(); + Assert.True(cache.HasBindAsyncMethod(parameterInfo)); + var methodFound = cache.FindBindAsyncMethod(parameterInfo); + + var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, + ParameterBindingMethodCache.HttpContextExpr).Compile(); + + var httpContext = new DefaultHttpContext(); + + var result = await parseHttpContext(httpContext); + Assert.NotNull(result); + Assert.IsType(result); + } + [Fact] public async Task FindBindAsyncMethod_FindsFallbackMethodWhenPreferredMethodsReturnTypeIsWrong() { @@ -627,7 +652,6 @@ private static void BindAsyncRecordMethod(BindAsyncRecord arg) { } private static void BindAsyncStructMethod(BindAsyncStruct arg) { } private static void BindAsyncNullableStructMethod(BindAsyncStruct? arg) { } private static void NullableReturningBindAsyncStructMethod(NullableReturningBindAsyncStruct arg) { } - private static void BindAsyncSingleArgRecordMethod(BindAsyncSingleArgRecord arg) { } private static void BindAsyncSingleArgStructMethod(BindAsyncSingleArgStruct arg) { } private static void InheritBindAsyncMethod(InheritBindAsync arg) { } @@ -639,6 +663,7 @@ private static void BindAsyncFromClassAndInterfaceMethod(BindAsyncFromClassAndIn private static void BindAsyncFromInterfaceWithParameterInfoMethod(BindAsyncFromInterfaceWithParameterInfo args) { } private static void BindAsyncFallbackMethod(BindAsyncFallsBack? arg) { } private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } + private static void BindAsyncFromStaticAbstractInterfaceMethod(BindAsyncFromStaticAbstractInterface arg) { } private static ParameterInfo GetFirstParameter(Expression> expr) { @@ -1347,6 +1372,14 @@ public RecordStructWithInvalidConstructors(int foo, int bar) } } + private class BindAsyncFromStaticAbstractInterface : IBindableFromHttpContext + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new()); + } + } + private class MockParameterInfo : ParameterInfo { public MockParameterInfo(Type type, string name) diff --git a/src/Shared/ParameterBindingMethodCache.cs b/src/Shared/ParameterBindingMethodCache.cs index 43ac79509890..dac7d3320399 100644 --- a/src/Shared/ParameterBindingMethodCache.cs +++ b/src/Shared/ParameterBindingMethodCache.cs @@ -24,6 +24,7 @@ internal sealed class ParameterBindingMethodCache { private static readonly MethodInfo ConvertValueTaskMethod = typeof(ParameterBindingMethodCache).GetMethod(nameof(ConvertValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; private static readonly MethodInfo ConvertValueTaskOfNullableResultMethod = typeof(ParameterBindingMethodCache).GetMethod(nameof(ConvertValueTaskOfNullableResult), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo BindAsyncMethod = typeof(ParameterBindingMethodCache).GetMethod(nameof(BindAsync), BindingFlags.NonPublic | BindingFlags.Static)!; internal static readonly ParameterExpression TempSourceStringExpr = Expression.Variable(typeof(string), "tempSourceString"); internal static readonly ParameterExpression HttpContextExpr = Expression.Parameter(typeof(HttpContext), "httpContext"); @@ -382,26 +383,24 @@ static bool ValidateReturnType(MethodInfo methodInfo) private static MethodInfo? GetIBindableFromHttpContextMethod(Type type) { // Check if parameter is bindable via static abstract method on IBindableFromHttpContext - // JonSkeet himself said this is the way (https://stackoverflow.com/a/4864565/405892) - // but if we find another less exceptiony way we should probably do that instead - var isBindableViaInterface = true; - try - { - var _ = typeof(IBindableFromHttpContext<>).MakeGenericType(type); - } - catch (ArgumentException) - { - isBindableViaInterface = false; - } + var isBindableViaInterface = type.GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBindableFromHttpContext<>) && i.GetGenericArguments()[0] == type); if (isBindableViaInterface) { - return type.GetMethod("BindAsync", BindingFlags.Public | BindingFlags.Static)!; + return BindAsyncMethod.MakeGenericMethod(type); } return null; } + private static ValueTask BindAsync(HttpContext httpContext, ParameterInfo parameter) + where TValue : IBindableFromHttpContext + { + return TValue.BindAsync(httpContext, parameter); + } + + private MethodInfo? GetStaticMethodFromHierarchy(Type type, string name, Type[] parameterTypes, Func validateReturnType) { bool IsMatch(MethodInfo? method) => method is not null && !method.IsAbstract && validateReturnType(method); From 50dc0fb9da890219ca35a6f4e08add12939693da Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 8 Apr 2022 14:02:08 -0700 Subject: [PATCH 05/13] Replace LINQ use with a foreach loop + tests --- .../test/ParameterBindingMethodCacheTests.cs | 53 +++++++++++++++---- src/Shared/ParameterBindingMethodCache.cs | 10 ++-- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index 32e14e633f99..296595ce37a4 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq.Expressions; using System.Reflection; @@ -321,9 +322,16 @@ public void HasBindAsyncMethod_ReturnsTrueForNullableReturningBindAsyncStructMet } [Fact] - public void HasBindAsyncMethod_ReturnsTrueForClassImplementingIBindableFromHttpContext() + public void HasBindAsyncMethod_ReturnsTrueForClassImplicitlyImplementingIBindableFromHttpContext() { - var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterface arg) => BindAsyncFromStaticAbstractInterfaceMethod(arg)); + var parameterInfo = GetFirstParameter((BindAsyncFromImplicitStaticAbstractInterface arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethod(arg)); + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } + + [Fact] + public void HasBindAsyncMethod_ReturnsTrueForClassExplicitlyImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromExplicitStaticAbstractInterface arg) => BindAsyncFromExplicitStaticAbstractInterfaceMethod(arg)); Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); } @@ -335,9 +343,9 @@ public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNul } [Fact] - public async Task FindBindAsyncMethod_FindsForClassImplementingIBindableFromHttpContext() + public async Task FindBindAsyncMethod_FindsForClassImplicitlyImplementingIBindableFromHttpContext() { - var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterface arg) => BindAsyncFromStaticAbstractInterfaceMethod(arg)); + var parameterInfo = GetFirstParameter((BindAsyncFromImplicitStaticAbstractInterface arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethod(arg)); var cache = new ParameterBindingMethodCache(); Assert.True(cache.HasBindAsyncMethod(parameterInfo)); var methodFound = cache.FindBindAsyncMethod(parameterInfo); @@ -349,7 +357,25 @@ public async Task FindBindAsyncMethod_FindsForClassImplementingIBindableFromHttp var result = await parseHttpContext(httpContext); Assert.NotNull(result); - Assert.IsType(result); + Assert.IsType(result); + } + + [Fact] + public async Task FindBindAsyncMethod_FindsForClassExplicitlyImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromExplicitStaticAbstractInterface arg) => BindAsyncFromExplicitStaticAbstractInterfaceMethod(arg)); + var cache = new ParameterBindingMethodCache(); + Assert.True(cache.HasBindAsyncMethod(parameterInfo)); + var methodFound = cache.FindBindAsyncMethod(parameterInfo); + + var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, + ParameterBindingMethodCache.HttpContextExpr).Compile(); + + var httpContext = new DefaultHttpContext(); + + var result = await parseHttpContext(httpContext); + Assert.NotNull(result); + Assert.IsType(result); } [Fact] @@ -663,7 +689,8 @@ private static void BindAsyncFromClassAndInterfaceMethod(BindAsyncFromClassAndIn private static void BindAsyncFromInterfaceWithParameterInfoMethod(BindAsyncFromInterfaceWithParameterInfo args) { } private static void BindAsyncFallbackMethod(BindAsyncFallsBack? arg) { } private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } - private static void BindAsyncFromStaticAbstractInterfaceMethod(BindAsyncFromStaticAbstractInterface arg) { } + private static void BindAsyncFromImplicitStaticAbstractInterfaceMethod(BindAsyncFromImplicitStaticAbstractInterface arg) { } + private static void BindAsyncFromExplicitStaticAbstractInterfaceMethod(BindAsyncFromExplicitStaticAbstractInterface arg) { } private static ParameterInfo GetFirstParameter(Expression> expr) { @@ -1372,11 +1399,19 @@ public RecordStructWithInvalidConstructors(int foo, int bar) } } - private class BindAsyncFromStaticAbstractInterface : IBindableFromHttpContext + private class BindAsyncFromImplicitStaticAbstractInterface : IBindableFromHttpContext + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new()); + } + } + + private class BindAsyncFromExplicitStaticAbstractInterface : IBindableFromHttpContext { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + static ValueTask IBindableFromHttpContext.BindAsync(HttpContext context, ParameterInfo parameter) { - return ValueTask.FromResult(new()); + return ValueTask.FromResult(new()); } } diff --git a/src/Shared/ParameterBindingMethodCache.cs b/src/Shared/ParameterBindingMethodCache.cs index dac7d3320399..175d53343084 100644 --- a/src/Shared/ParameterBindingMethodCache.cs +++ b/src/Shared/ParameterBindingMethodCache.cs @@ -383,12 +383,12 @@ static bool ValidateReturnType(MethodInfo methodInfo) private static MethodInfo? GetIBindableFromHttpContextMethod(Type type) { // Check if parameter is bindable via static abstract method on IBindableFromHttpContext - var isBindableViaInterface = type.GetInterfaces() - .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBindableFromHttpContext<>) && i.GetGenericArguments()[0] == type); - - if (isBindableViaInterface) + foreach (var i in type.GetInterfaces()) { - return BindAsyncMethod.MakeGenericMethod(type); + if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBindableFromHttpContext<>) && i.GetGenericArguments()[0] == type) + { + return BindAsyncMethod.MakeGenericMethod(type); + } } return null; From 08daeb05ee181b8562710ac22bd2a8302853463b Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 8 Apr 2022 14:56:16 -0700 Subject: [PATCH 06/13] Another test --- .../test/ParameterBindingMethodCacheTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index 296595ce37a4..49a2eadf9c73 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -550,6 +550,7 @@ public static TheoryData InvalidBindAsyncTypesData typeof(BindAsyncWithParameterInfoWrongTypeInherit), typeof(BindAsyncWrongTypeFromInterface), typeof(BindAsyncBothBadMethods), + typeof(BindAsyncFromStaticAbstractInterfaceWrongType) }; } } @@ -691,6 +692,7 @@ private static void BindAsyncFallbackMethod(BindAsyncFallsBack? arg) { } private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } private static void BindAsyncFromImplicitStaticAbstractInterfaceMethod(BindAsyncFromImplicitStaticAbstractInterface arg) { } private static void BindAsyncFromExplicitStaticAbstractInterfaceMethod(BindAsyncFromExplicitStaticAbstractInterface arg) { } + private static void BindAsyncFromStaticAbstractInterfaceWrongTypeMethod(BindAsyncFromStaticAbstractInterfaceWrongType arg) { } private static ParameterInfo GetFirstParameter(Expression> expr) { @@ -1415,6 +1417,14 @@ private class BindAsyncFromExplicitStaticAbstractInterface : IBindableFromHttpCo } } + private class BindAsyncFromStaticAbstractInterfaceWrongType : IBindableFromHttpContext + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new()); + } + } + private class MockParameterInfo : ParameterInfo { public MockParameterInfo(Type type, string name) From 08107c3562778ba6e38faabd4d306948bb08ff4f Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 12 Apr 2022 14:17:25 -0700 Subject: [PATCH 07/13] Added tests for structs implementing IBindableFromHttpContext --- .../src/IBindableFromHttpContextOfT.cs | 14 ++-- .../test/ParameterBindingMethodCacheTests.cs | 79 ++++++++++++++++++- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs index c5c8061d148e..a405737ee0f3 100644 --- a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs +++ b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Http; /// The type that implements this interface. public interface IBindableFromHttpContext where TSelf : IBindableFromHttpContext { - /// - /// Creates an instance of from the . - /// - /// The for the current request. - /// The for the parameter of the route handler delegate the returned instance will populate. - /// The instance of . - static abstract ValueTask BindAsync(HttpContext context, ParameterInfo parameter); + /// + /// Creates an instance of from the . + /// + /// The for the current request. + /// The for the parameter of the route handler delegate the returned instance will populate. + /// The instance of . + static abstract ValueTask BindAsync(HttpContext context, ParameterInfo parameter); } diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index 49a2eadf9c73..50aea0ae0022 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -335,6 +335,20 @@ public void HasBindAsyncMethod_ReturnsTrueForClassExplicitlyImplementingIBindabl Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); } + [Fact] + public void HasBindAsyncMethod_ReturnsTrueForNonNullableStructImplicitlyImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructImplicitStaticAbstractInterfaceMethod(arg)); + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } + + [Fact] + public void HasBindAsyncMethod_ReturnsTrueForNonNullableStructExplicitlyImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructExplicitStaticAbstractInterfaceMethod(arg)); + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } + [Fact] public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNullableType() { @@ -378,6 +392,42 @@ public async Task FindBindAsyncMethod_FindsForClassExplicitlyImplementingIBindab Assert.IsType(result); } + [Fact] + public async Task FindBindAsyncMethod_FindsForNonNullableStructImplicitlyImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructImplicitStaticAbstractInterfaceMethod(arg)); + var cache = new ParameterBindingMethodCache(); + Assert.True(cache.HasBindAsyncMethod(parameterInfo)); + var methodFound = cache.FindBindAsyncMethod(parameterInfo); + + var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, + ParameterBindingMethodCache.HttpContextExpr).Compile(); + + var httpContext = new DefaultHttpContext(); + + var result = await parseHttpContext(httpContext); + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public async Task FindBindAsyncMethod_FindsForNonNullableStructExplicitlyImplementingIBindableFromHttpContext() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructExplicitStaticAbstractInterfaceMethod(arg)); + var cache = new ParameterBindingMethodCache(); + Assert.True(cache.HasBindAsyncMethod(parameterInfo)); + var methodFound = cache.FindBindAsyncMethod(parameterInfo); + + var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, + ParameterBindingMethodCache.HttpContextExpr).Compile(); + + var httpContext = new DefaultHttpContext(); + + var result = await parseHttpContext(httpContext); + Assert.NotNull(result); + Assert.IsType(result); + } + [Fact] public async Task FindBindAsyncMethod_FindsFallbackMethodWhenPreferredMethodsReturnTypeIsWrong() { @@ -550,7 +600,8 @@ public static TheoryData InvalidBindAsyncTypesData typeof(BindAsyncWithParameterInfoWrongTypeInherit), typeof(BindAsyncWrongTypeFromInterface), typeof(BindAsyncBothBadMethods), - typeof(BindAsyncFromStaticAbstractInterfaceWrongType) + typeof(BindAsyncFromStaticAbstractInterfaceWrongType), + typeof(NonNullableBindAsyncFromStaticAbstractInterfaceStructWrongType) }; } } @@ -693,6 +744,8 @@ private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } private static void BindAsyncFromImplicitStaticAbstractInterfaceMethod(BindAsyncFromImplicitStaticAbstractInterface arg) { } private static void BindAsyncFromExplicitStaticAbstractInterfaceMethod(BindAsyncFromExplicitStaticAbstractInterface arg) { } private static void BindAsyncFromStaticAbstractInterfaceWrongTypeMethod(BindAsyncFromStaticAbstractInterfaceWrongType arg) { } + private static void BindAsyncFromNonNullableStructImplicitStaticAbstractInterfaceMethod(BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct arg) { } + private static void BindAsyncFromNonNullableStructExplicitStaticAbstractInterfaceMethod(BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct arg) { } private static ParameterInfo GetFirstParameter(Expression> expr) { @@ -1425,6 +1478,30 @@ private class BindAsyncFromStaticAbstractInterfaceWrongType : IBindableFromHttpC } } + private record struct BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct() : IBindableFromHttpContext + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new()); + } + } + + private record struct BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct() : IBindableFromHttpContext + { + static ValueTask IBindableFromHttpContext.BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new()); + } + } + + private record struct NonNullableBindAsyncFromStaticAbstractInterfaceStructWrongType() : IBindableFromHttpContext + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new()); + } + } + private class MockParameterInfo : ParameterInfo { public MockParameterInfo(Type type, string name) From 47cf7c2f91655c9bf599609ec00c3ec08d705b46 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Tue, 12 Apr 2022 14:57:20 -0700 Subject: [PATCH 08/13] PR feedback --- .../src/IBindableFromHttpContextOfT.cs | 2 +- .../test/ParameterBindingMethodCacheTests.cs | 79 +------------------ src/Shared/ParameterBindingMethodCache.cs | 2 +- 3 files changed, 3 insertions(+), 80 deletions(-) diff --git a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs index a405737ee0f3..806c71cd65a6 100644 --- a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs +++ b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Http; /// route handler delegate. /// /// The type that implements this interface. -public interface IBindableFromHttpContext where TSelf : IBindableFromHttpContext +public interface IBindableFromHttpContext where TSelf : class?, IBindableFromHttpContext { /// /// Creates an instance of from the . diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index 50aea0ae0022..49a2eadf9c73 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -335,20 +335,6 @@ public void HasBindAsyncMethod_ReturnsTrueForClassExplicitlyImplementingIBindabl Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); } - [Fact] - public void HasBindAsyncMethod_ReturnsTrueForNonNullableStructImplicitlyImplementingIBindableFromHttpContext() - { - var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructImplicitStaticAbstractInterfaceMethod(arg)); - Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); - } - - [Fact] - public void HasBindAsyncMethod_ReturnsTrueForNonNullableStructExplicitlyImplementingIBindableFromHttpContext() - { - var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructExplicitStaticAbstractInterfaceMethod(arg)); - Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); - } - [Fact] public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNullableType() { @@ -392,42 +378,6 @@ public async Task FindBindAsyncMethod_FindsForClassExplicitlyImplementingIBindab Assert.IsType(result); } - [Fact] - public async Task FindBindAsyncMethod_FindsForNonNullableStructImplicitlyImplementingIBindableFromHttpContext() - { - var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructImplicitStaticAbstractInterfaceMethod(arg)); - var cache = new ParameterBindingMethodCache(); - Assert.True(cache.HasBindAsyncMethod(parameterInfo)); - var methodFound = cache.FindBindAsyncMethod(parameterInfo); - - var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, - ParameterBindingMethodCache.HttpContextExpr).Compile(); - - var httpContext = new DefaultHttpContext(); - - var result = await parseHttpContext(httpContext); - Assert.NotNull(result); - Assert.IsType(result); - } - - [Fact] - public async Task FindBindAsyncMethod_FindsForNonNullableStructExplicitlyImplementingIBindableFromHttpContext() - { - var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct arg) => BindAsyncFromNonNullableStructExplicitStaticAbstractInterfaceMethod(arg)); - var cache = new ParameterBindingMethodCache(); - Assert.True(cache.HasBindAsyncMethod(parameterInfo)); - var methodFound = cache.FindBindAsyncMethod(parameterInfo); - - var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, - ParameterBindingMethodCache.HttpContextExpr).Compile(); - - var httpContext = new DefaultHttpContext(); - - var result = await parseHttpContext(httpContext); - Assert.NotNull(result); - Assert.IsType(result); - } - [Fact] public async Task FindBindAsyncMethod_FindsFallbackMethodWhenPreferredMethodsReturnTypeIsWrong() { @@ -600,8 +550,7 @@ public static TheoryData InvalidBindAsyncTypesData typeof(BindAsyncWithParameterInfoWrongTypeInherit), typeof(BindAsyncWrongTypeFromInterface), typeof(BindAsyncBothBadMethods), - typeof(BindAsyncFromStaticAbstractInterfaceWrongType), - typeof(NonNullableBindAsyncFromStaticAbstractInterfaceStructWrongType) + typeof(BindAsyncFromStaticAbstractInterfaceWrongType) }; } } @@ -744,8 +693,6 @@ private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } private static void BindAsyncFromImplicitStaticAbstractInterfaceMethod(BindAsyncFromImplicitStaticAbstractInterface arg) { } private static void BindAsyncFromExplicitStaticAbstractInterfaceMethod(BindAsyncFromExplicitStaticAbstractInterface arg) { } private static void BindAsyncFromStaticAbstractInterfaceWrongTypeMethod(BindAsyncFromStaticAbstractInterfaceWrongType arg) { } - private static void BindAsyncFromNonNullableStructImplicitStaticAbstractInterfaceMethod(BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct arg) { } - private static void BindAsyncFromNonNullableStructExplicitStaticAbstractInterfaceMethod(BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct arg) { } private static ParameterInfo GetFirstParameter(Expression> expr) { @@ -1478,30 +1425,6 @@ private class BindAsyncFromStaticAbstractInterfaceWrongType : IBindableFromHttpC } } - private record struct BindAsyncFromStaticAbstractImplicitInterfaceNonNullableStruct() : IBindableFromHttpContext - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - return ValueTask.FromResult(new()); - } - } - - private record struct BindAsyncFromStaticAbstractExplicitInterfaceNonNullableStruct() : IBindableFromHttpContext - { - static ValueTask IBindableFromHttpContext.BindAsync(HttpContext context, ParameterInfo parameter) - { - return ValueTask.FromResult(new()); - } - } - - private record struct NonNullableBindAsyncFromStaticAbstractInterfaceStructWrongType() : IBindableFromHttpContext - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - return ValueTask.FromResult(new()); - } - } - private class MockParameterInfo : ParameterInfo { public MockParameterInfo(Type type, string name) diff --git a/src/Shared/ParameterBindingMethodCache.cs b/src/Shared/ParameterBindingMethodCache.cs index 175d53343084..3f2e33599474 100644 --- a/src/Shared/ParameterBindingMethodCache.cs +++ b/src/Shared/ParameterBindingMethodCache.cs @@ -395,7 +395,7 @@ static bool ValidateReturnType(MethodInfo methodInfo) } private static ValueTask BindAsync(HttpContext httpContext, ParameterInfo parameter) - where TValue : IBindableFromHttpContext + where TValue : class?, IBindableFromHttpContext { return TValue.BindAsync(httpContext, parameter); } From 4beb2195d870814e3b828df50429811f23460b13 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 20 May 2022 09:55:04 -0700 Subject: [PATCH 09/13] Remove nullability from generic type arg on IBindableFromHttpContext --- src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs index 806c71cd65a6..0f414b849e5f 100644 --- a/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs +++ b/src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Http; /// route handler delegate. /// /// The type that implements this interface. -public interface IBindableFromHttpContext where TSelf : class?, IBindableFromHttpContext +public interface IBindableFromHttpContext where TSelf : class, IBindableFromHttpContext { /// /// Creates an instance of from the . From a0263ba143d2da49c9b622af783acb092736cdee Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 20 May 2022 16:08:16 -0700 Subject: [PATCH 10/13] Update src/Shared/ParameterBindingMethodCache.cs Co-authored-by: Stephen Halter --- src/Shared/ParameterBindingMethodCache.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Shared/ParameterBindingMethodCache.cs b/src/Shared/ParameterBindingMethodCache.cs index 3f2e33599474..eb5750a8fd38 100644 --- a/src/Shared/ParameterBindingMethodCache.cs +++ b/src/Shared/ParameterBindingMethodCache.cs @@ -400,7 +400,6 @@ static bool ValidateReturnType(MethodInfo methodInfo) return TValue.BindAsync(httpContext, parameter); } - private MethodInfo? GetStaticMethodFromHierarchy(Type type, string name, Type[] parameterTypes, Func validateReturnType) { bool IsMatch(MethodInfo? method) => method is not null && !method.IsAbstract && validateReturnType(method); From 3e86f3044e87e628b44494d395f8a7fe523d4e29 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 10 Jun 2022 10:13:01 -0700 Subject: [PATCH 11/13] Update public API --- src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt index b8d0ab94d187..a307310dc1af 100644 --- a/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ 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! Microsoft.AspNetCore.Http.EndpointMetadataCollection.GetRequiredMetadata() -> T! +Microsoft.AspNetCore.Http.IBindableFromHttpContext +Microsoft.AspNetCore.Http.IBindableFromHttpContext.BindAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Reflection.ParameterInfo! parameter) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string? From ec3a9883f146b927eb38a7910dfa497413997b85 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 10 Jun 2022 13:47:11 -0700 Subject: [PATCH 12/13] Add tests from PR feedback --- .../test/ParameterBindingMethodCacheTests.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index 49a2eadf9c73..468e02851447 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -303,6 +303,10 @@ public static IEnumerable BindAsyncParameterInfoData { GetFirstParameter((BindAsyncFromInterfaceWithParameterInfo arg) => BindAsyncFromInterfaceWithParameterInfoMethod(arg)) }, + new[] + { + GetFirstParameter((BindAsyncFromStaticAbstractInterfaceAndBindAsync arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(arg)) + }, }; } } @@ -335,6 +339,13 @@ public void HasBindAsyncMethod_ReturnsTrueForClassExplicitlyImplementingIBindabl Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); } + [Fact] + public void HasBindAsyncMethod_ReturnsTrueForClassImplementingIBindableFromHttpContextAndNonInterfaceBindAsyncMethod() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterfaceAndBindAsync arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(arg)); + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } + [Fact] public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNullableType() { @@ -410,6 +421,25 @@ public async Task FindBindAsyncMethod_FindsFallbackMethodFromInheritedWhenPrefer Assert.Null(await parseHttpContext(httpContext)); } + [Fact] + public async Task FindBindAsyncMethod_FindsMethodFromStaticAbstractInterfaceWhenValidNonInterfaceMethodAlsoExists() + { + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterfaceAndBindAsync? arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(arg)); + var cache = new ParameterBindingMethodCache(); + Assert.True(cache.HasBindAsyncMethod(parameterInfo)); + var methodFound = cache.FindBindAsyncMethod(parameterInfo); + + var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, + ParameterBindingMethodCache.HttpContextExpr).Compile(); + + var httpContext = new DefaultHttpContext(); + var result = await parseHttpContext(httpContext); + + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(BindAsyncSource.InterfaceStaticAbstractImplicit, ((BindAsyncFromStaticAbstractInterfaceAndBindAsync)result).BoundFrom); + } + [Theory] [InlineData(typeof(ClassWithParameterlessConstructor))] [InlineData(typeof(RecordClassParameterlessConstructor))] @@ -692,6 +722,7 @@ private static void BindAsyncFallbackMethod(BindAsyncFallsBack? arg) { } private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } private static void BindAsyncFromImplicitStaticAbstractInterfaceMethod(BindAsyncFromImplicitStaticAbstractInterface arg) { } private static void BindAsyncFromExplicitStaticAbstractInterfaceMethod(BindAsyncFromExplicitStaticAbstractInterface arg) { } + private static void BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(BindAsyncFromStaticAbstractInterfaceAndBindAsync arg) { } private static void BindAsyncFromStaticAbstractInterfaceWrongTypeMethod(BindAsyncFromStaticAbstractInterfaceWrongType arg) { } private static ParameterInfo GetFirstParameter(Expression> expr) @@ -700,6 +731,12 @@ private static ParameterInfo GetFirstParameter(Expression> expr) return mc.Method.GetParameters()[0]; } + private static ParameterInfo GetParameterAtIndex(Expression> expr, int paramIndex) + { + var mc = (MethodCallExpression)expr.Body; + return mc.Method.GetParameters()[paramIndex]; + } + private record TryParseStringRecord(int Value) { public static bool TryParse(string? value, IFormatProvider formatProvider, out TryParseStringRecord? result) @@ -1417,6 +1454,28 @@ private class BindAsyncFromExplicitStaticAbstractInterface : IBindableFromHttpCo } } + private class BindAsyncFromStaticAbstractInterfaceAndBindAsync : IBindableFromHttpContext + { + public BindAsyncFromStaticAbstractInterfaceAndBindAsync(BindAsyncSource boundFrom) + { + BoundFrom = boundFrom; + } + + public BindAsyncSource BoundFrom { get; } + + // Implicit interface implementation + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + return ValueTask.FromResult(new(BindAsyncSource.InterfaceStaticAbstractImplicit)); + } + + // Late-bound pattern based match in RequestDelegateFactory + public static ValueTask BindAsync(HttpContext context) + { + return ValueTask.FromResult(new(BindAsyncSource.Reflection)); + } + } + private class BindAsyncFromStaticAbstractInterfaceWrongType : IBindableFromHttpContext { public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) @@ -1425,6 +1484,13 @@ private class BindAsyncFromStaticAbstractInterfaceWrongType : IBindableFromHttpC } } + private enum BindAsyncSource + { + Reflection, + InterfaceStaticAbstractImplicit, + InterfaceStaticAbstractExplicit + } + private class MockParameterInfo : ParameterInfo { public MockParameterInfo(Type type, string name) From 1afeeb8ea480a6d70b23a5a98599a349b81d74d1 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Fri, 10 Jun 2022 14:46:30 -0700 Subject: [PATCH 13/13] Fix arg nullability --- .../Http.Extensions/test/ParameterBindingMethodCacheTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index 468e02851447..725fb9587855 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -424,7 +424,7 @@ public async Task FindBindAsyncMethod_FindsFallbackMethodFromInheritedWhenPrefer [Fact] public async Task FindBindAsyncMethod_FindsMethodFromStaticAbstractInterfaceWhenValidNonInterfaceMethodAlsoExists() { - var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterfaceAndBindAsync? arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(arg)); + var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterfaceAndBindAsync arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(arg)); var cache = new ParameterBindingMethodCache(); Assert.True(cache.HasBindAsyncMethod(parameterInfo)); var methodFound = cache.FindBindAsyncMethod(parameterInfo);