Skip to content

Introduce IBindableFromHttpContext<TSelf> #41100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/Http/Http.Abstractions/src/IBindableFromHttpContextOfT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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;

/// <summary>
/// Defines a mechanism for creating an instance of a type from an <see cref="HttpContext"/> when binding parameters for an endpoint
/// route handler delegate.
/// </summary>
/// <typeparam name="TSelf">The type that implements this interface.</typeparam>
public interface IBindableFromHttpContext<TSelf> where TSelf : class, IBindableFromHttpContext<TSelf>
{
/// <summary>
/// Creates an instance of <typeparamref name="TSelf"/> from the <see cref="HttpContext"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
/// <param name="parameter">The <see cref="ParameterInfo"/> for the parameter of the route handler delegate the returned instance will populate.</param>
/// <returns>The instance of <typeparamref name="TSelf"/>.</returns>
static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}
2 changes: 2 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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>() -> T!
Microsoft.AspNetCore.Http.IBindableFromHttpContext<TSelf>
Microsoft.AspNetCore.Http.IBindableFromHttpContext<TSelf>.BindAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Reflection.ParameterInfo! parameter) -> System.Threading.Tasks.ValueTask<TSelf?>
Microsoft.AspNetCore.Http.IRouteHandlerFilter.InvokeAsync(Microsoft.AspNetCore.Http.RouteHandlerInvocationContext! context, Microsoft.AspNetCore.Http.RouteHandlerFilterDelegate! next) -> System.Threading.Tasks.ValueTask<object?>
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata
Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata.Name.get -> string?
Expand Down
146 changes: 145 additions & 1 deletion src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable enable

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
Expand Down Expand Up @@ -302,6 +303,10 @@ public static IEnumerable<object[]> BindAsyncParameterInfoData
{
GetFirstParameter((BindAsyncFromInterfaceWithParameterInfo arg) => BindAsyncFromInterfaceWithParameterInfoMethod(arg))
},
new[]
{
GetFirstParameter((BindAsyncFromStaticAbstractInterfaceAndBindAsync arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(arg))
},
};
}
}
Expand All @@ -320,13 +325,70 @@ public void HasBindAsyncMethod_ReturnsTrueForNullableReturningBindAsyncStructMet
Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo));
}

[Fact]
public void HasBindAsyncMethod_ReturnsTrueForClassImplicitlyImplementingIBindableFromHttpContext()
{
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));
}

[Fact]
public void HasBindAsyncMethod_ReturnsTrueForClassImplementingIBindableFromHttpContextAndNonInterfaceBindAsyncMethod()
{
var parameterInfo = GetFirstParameter((BindAsyncFromStaticAbstractInterfaceAndBindAsync arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethodInsteadOfReflectionMatchedMethod(arg));
Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo));
}

[Fact]
public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNullableType()
{
var parameterInfo = GetFirstParameter((BindAsyncStruct? arg) => BindAsyncNullableStructMethod(arg));
Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo));
}

[Fact]
public async Task FindBindAsyncMethod_FindsForClassImplicitlyImplementingIBindableFromHttpContext()
{
var parameterInfo = GetFirstParameter((BindAsyncFromImplicitStaticAbstractInterface arg) => BindAsyncFromImplicitStaticAbstractInterfaceMethod(arg));
var cache = new ParameterBindingMethodCache();
Assert.True(cache.HasBindAsyncMethod(parameterInfo));
var methodFound = cache.FindBindAsyncMethod(parameterInfo);

var parseHttpContext = Expression.Lambda<Func<HttpContext, ValueTask<object?>>>(methodFound.Expression!,
ParameterBindingMethodCache.HttpContextExpr).Compile();

var httpContext = new DefaultHttpContext();

var result = await parseHttpContext(httpContext);
Assert.NotNull(result);
Assert.IsType<BindAsyncFromImplicitStaticAbstractInterface>(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<Func<HttpContext, ValueTask<object?>>>(methodFound.Expression!,
ParameterBindingMethodCache.HttpContextExpr).Compile();

var httpContext = new DefaultHttpContext();

var result = await parseHttpContext(httpContext);
Assert.NotNull(result);
Assert.IsType<BindAsyncFromExplicitStaticAbstractInterface>(result);
}

[Fact]
public async Task FindBindAsyncMethod_FindsFallbackMethodWhenPreferredMethodsReturnTypeIsWrong()
{
Expand Down Expand Up @@ -359,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<Func<HttpContext, ValueTask<object>>>(methodFound.Expression!,
ParameterBindingMethodCache.HttpContextExpr).Compile();

var httpContext = new DefaultHttpContext();
var result = await parseHttpContext(httpContext);

Assert.NotNull(result);
Assert.IsType<BindAsyncFromStaticAbstractInterfaceAndBindAsync>(result);
Assert.Equal(BindAsyncSource.InterfaceStaticAbstractImplicit, ((BindAsyncFromStaticAbstractInterfaceAndBindAsync)result).BoundFrom);
}

[Theory]
[InlineData(typeof(ClassWithParameterlessConstructor))]
[InlineData(typeof(RecordClassParameterlessConstructor))]
Expand Down Expand Up @@ -499,6 +580,7 @@ public static TheoryData<Type> InvalidBindAsyncTypesData
typeof(BindAsyncWithParameterInfoWrongTypeInherit),
typeof(BindAsyncWrongTypeFromInterface),
typeof(BindAsyncBothBadMethods),
typeof(BindAsyncFromStaticAbstractInterfaceWrongType)
};
}
}
Expand Down Expand Up @@ -627,7 +709,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) { }
Expand All @@ -639,13 +720,23 @@ 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 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<T>(Expression<Action<T>> expr)
{
var mc = (MethodCallExpression)expr.Body;
return mc.Method.GetParameters()[0];
}

private static ParameterInfo GetParameterAtIndex<T>(Expression<Action<T>> 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)
Expand Down Expand Up @@ -1347,6 +1438,59 @@ public RecordStructWithInvalidConstructors(int foo, int bar)
}
}

private class BindAsyncFromImplicitStaticAbstractInterface : IBindableFromHttpContext<BindAsyncFromImplicitStaticAbstractInterface>
{
public static ValueTask<BindAsyncFromImplicitStaticAbstractInterface?> BindAsync(HttpContext context, ParameterInfo parameter)
{
return ValueTask.FromResult<BindAsyncFromImplicitStaticAbstractInterface?>(new());
}
}

private class BindAsyncFromExplicitStaticAbstractInterface : IBindableFromHttpContext<BindAsyncFromExplicitStaticAbstractInterface>
{
static ValueTask<BindAsyncFromExplicitStaticAbstractInterface?> IBindableFromHttpContext<BindAsyncFromExplicitStaticAbstractInterface>.BindAsync(HttpContext context, ParameterInfo parameter)
{
return ValueTask.FromResult<BindAsyncFromExplicitStaticAbstractInterface?>(new());
}
}

private class BindAsyncFromStaticAbstractInterfaceAndBindAsync : IBindableFromHttpContext<BindAsyncFromStaticAbstractInterfaceAndBindAsync>
{
public BindAsyncFromStaticAbstractInterfaceAndBindAsync(BindAsyncSource boundFrom)
{
BoundFrom = boundFrom;
}

public BindAsyncSource BoundFrom { get; }

// Implicit interface implementation
public static ValueTask<BindAsyncFromStaticAbstractInterfaceAndBindAsync?> BindAsync(HttpContext context, ParameterInfo parameter)
{
return ValueTask.FromResult<BindAsyncFromStaticAbstractInterfaceAndBindAsync?>(new(BindAsyncSource.InterfaceStaticAbstractImplicit));
}

// Late-bound pattern based match in RequestDelegateFactory
public static ValueTask<BindAsyncFromStaticAbstractInterfaceAndBindAsync?> BindAsync(HttpContext context)
{
return ValueTask.FromResult<BindAsyncFromStaticAbstractInterfaceAndBindAsync?>(new(BindAsyncSource.Reflection));
}
}

private class BindAsyncFromStaticAbstractInterfaceWrongType : IBindableFromHttpContext<BindAsyncFromImplicitStaticAbstractInterface>
{
public static ValueTask<BindAsyncFromImplicitStaticAbstractInterface?> BindAsync(HttpContext context, ParameterInfo parameter)
{
return ValueTask.FromResult<BindAsyncFromImplicitStaticAbstractInterface?>(new());
}
}

private enum BindAsyncSource
{
Reflection,
InterfaceStaticAbstractImplicit,
InterfaceStaticAbstractExplicit
}

private class MockParameterInfo : ParameterInfo
{
public MockParameterInfo(Type type, string name)
Expand Down
14 changes: 14 additions & 0 deletions src/Http/samples/MinimalSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -63,6 +64,19 @@

});

app.MapPost("/todos", (TodoBindable todo) => todo);

app.Run();

internal record Todo(int Id, string Title);
public class TodoBindable : IBindableFromHttpContext<TodoBindable>
{
public int Id { get; set; }
public string Title { get; set; }
public bool IsComplete { get; set; }

public static ValueTask<TodoBindable> BindAsync(HttpContext context, ParameterInfo parameter)
{
return ValueTask.FromResult(new TodoBindable { Id = 1, Title = "I was bound from IBindableFromHttpContext<TodoBindable>.BindAsync!" });
}
}
35 changes: 31 additions & 4 deletions src/Shared/ParameterBindingMethodCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -185,12 +186,18 @@ static bool ValidateReturnType(MethodInfo methodInfo)
(Func<ParameterInfo, Expression>?, int) Finder(Type 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);
var methodInfo = GetIBindableFromHttpContextMethod(nonNullableParameterType);

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:
Expand Down Expand Up @@ -373,6 +380,26 @@ 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<TSelf>
foreach (var i in type.GetInterfaces())
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IBindableFromHttpContext<>) && i.GetGenericArguments()[0] == type)
{
return BindAsyncMethod.MakeGenericMethod(type);
}
}

return null;
}

private static ValueTask<TValue?> BindAsync<TValue>(HttpContext httpContext, ParameterInfo parameter)
where TValue : class?, IBindableFromHttpContext<TValue>
{
return TValue.BindAsync(httpContext, parameter);
}

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);
Expand Down