Use Type.GetMethod in ViewComponentMethodSelector

Fixes #3211
This commit is contained in:
Pranav K 2015-10-05 18:32:32 -07:00
parent d9e40d92de
commit 30f5826380
4 changed files with 330 additions and 27 deletions

View File

@ -858,6 +858,22 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
return string.Format(CultureInfo.CurrentCulture, GetString("Dictionary_DuplicateKey"), p0);
}
/// <summary>
/// The view component method '{0}' cannot return a {1}.
/// </summary>
internal static string ViewComponent_SyncMethod_CannotReturnTask
{
get { return GetString("ViewComponent_SyncMethod_CannotReturnTask"); }
}
/// <summary>
/// The view component method '{0}' cannot return a {1}.
/// </summary>
internal static string FormatViewComponent_SyncMethod_CannotReturnTask(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_SyncMethod_CannotReturnTask"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -277,4 +277,7 @@
<data name="Dictionary_DuplicateKey" xml:space="preserve">
<value>The collection already contains an entry with key '{0}'.</value>
</data>
<data name="ViewComponent_SyncMethod_CannotReturnTask" xml:space="preserve">
<value>The view component method '{0}' cannot return a {1}.</value>
</data>
</root>

View File

@ -55,42 +55,44 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new InvalidOperationException(
Resources.FormatViewComponent_SyncMethod_ShouldReturnValue(SyncMethodName));
}
else if (method.ReturnType.IsAssignableFrom(typeof(Task)))
{
throw new InvalidOperationException(
Resources.FormatViewComponent_SyncMethod_CannotReturnTask(SyncMethodName, nameof(Task)));
}
return method;
}
private static MethodInfo GetMethod(TypeInfo componentType, object[] args, string methodName)
{
args = args ?? new object[0];
var argumentExpressions = new Expression[args.Length];
for (var i = 0; i < args.Length; i++)
Type[] types;
if (args == null || args.Length == 0)
{
argumentExpressions[i] = Expression.Constant(args[i], args[i].GetType());
types = Type.EmptyTypes;
}
else
{
types = new Type[args.Length];
for (var i = 0; i < args.Length; i++)
{
types[i] = args[i]?.GetType() ?? typeof(object);
}
}
try
{
// We're currently using this technique to make a call into a component method that looks like a
// regular method call.
//
// Ex: @Component.Invoke<Cart>("hello", 5) => cart.Invoke("hello", 5)
//
// This approach has some drawbacks, namely it doesn't account for default parameters, and more
// noticably, it throws if the method is not found.
//
// Unfortunely the overload of Type.GetMethod that we would like to use is not present in CoreCLR.
// Item #160 in Jira tracks these issues.
var expression = Expression.Call(
Expression.Constant(null, componentType.AsType()),
methodName,
null,
argumentExpressions);
return expression.Method;
}
catch (InvalidOperationException)
{
return null;
}
#if DNX451
return componentType.AsType().GetMethod(
methodName,
BindingFlags.Public | BindingFlags.Instance,
binder: null,
types: types,
modifiers: null);
#else
var method = componentType.AsType().GetMethod(methodName, types: types);
// At most one method (including static and instance methods) with the same parameter types can exist
// per type.
return method != null && method.IsStatic ? null : method;
#endif
}
}
}

View File

@ -0,0 +1,282 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
public class ViewComponentMethodSelectorTest
{
[Theory]
[InlineData(typeof(ViewComponentWithSyncInvoke), new object[] { "" })]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[] { 42, false })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { "" })]
public void FindAsyncMethod_ReturnsNull_IfMatchCannotBeFound(Type type, object[] args)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindAsyncMethod(typeInfo, args);
// Assert
Assert.Null(method);
}
[Theory]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[0])]
[InlineData(typeof(ViewComponentWithSyncInvoke), new object[] { "" })]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[] { "" })]
[InlineData(typeof(ViewComponentWithSyncInvoke), new object[] { 42 })]
[InlineData(typeof(ViewComponentWithAsyncInvoke), new object[] { "", 42 })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { })]
[InlineData(typeof(ViewComponentWithNonPublicNonInstanceInvokes), new object[] { "" })]
[InlineData(typeof(BaseClass), new object[] { })]
public void FindSyncMethod_ReturnsNull_IfMatchCannotBeFound(Type type, object[] args)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindSyncMethod(typeInfo, args);
// Assert
Assert.Null(method);
}
[Theory]
[InlineData(new object[] { new object[] { "Hello" } })]
[InlineData(new object[] { new object[] { 4 } })]
[InlineData(new object[] { new object[] { "", 5 } })]
public void FindAsyncMethod_ThrowsIfInvokeAsyncDoesNotHaveCorrectReturnType(object[] args)
{
// Arrange
var typeInfo = typeof(TypeWithInvalidInvokeAsync).GetTypeInfo();
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(
() => ViewComponentMethodSelector.FindAsyncMethod(typeInfo, args));
Assert.Equal("The async view component method 'InvokeAsync' should be declared to return Task<T>.",
ex.Message);
}
[Fact]
public void FindSyncMethod_ThrowsIfInvokeSyncIsAVoidMethod()
{
// Arrange
var expectedMessage = "The view component method 'Invoke' should be declared to return a value.";
var typeInfo = typeof(TypeWithInvalidInvokeSync).GetTypeInfo();
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(
() => ViewComponentMethodSelector.FindSyncMethod(typeInfo, new object[] { 4 }));
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public void FindSyncMethod_ThrowsIfInvokeSyncReturnsTask()
{
// Arrange
var expectedMessage = "The view component method 'Invoke' cannot return a Task.";
var typeInfo = typeof(TypeWithInvalidInvokeSync).GetTypeInfo();
// Act and Assert
var ex = Assert.Throws<InvalidOperationException>(
() => ViewComponentMethodSelector.FindSyncMethod(typeInfo, new object[] { "" }));
Assert.Equal(expectedMessage, ex.Message);
}
public static TheoryData FindAsyncMethod_ReturnsMethodMatchingParametersData
{
get
{
var derivedClass = new DerivedClass();
return new TheoryData<Type, object[], string>
{
{ typeof(ViewComponentWithAsyncInvoke), new object[] { "", }, "1" },
{ typeof(ViewComponentWithAsyncInvoke), new object[] { "", 2 }, "2" },
{ typeof(ViewComponentWithAsyncInvoke), new object[] { "", 0, 1 }, "3" },
{ typeof(ViewComponentWithAsyncInvoke), new object[] { 1, false, 1 }, "4" },
{ typeof(MethodsWithValueConversions), new object[] { 2, (byte)1, (byte)2 }, "2" },
{ typeof(MethodsWithValueConversions), new object[] { derivedClass, derivedClass }, "4" },
{ typeof(MethodsWithValueConversions), new object[] { CultureInfo.InvariantCulture }, "6" },
};
}
}
[Theory]
[MemberData(nameof(FindAsyncMethod_ReturnsMethodMatchingParametersData))]
public void FindAsyncMethod_ReturnsMethodMatchingParameters(Type type, object[] args, string expectedId)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindAsyncMethod(typeInfo, args);
// Assert
Assert.NotNull(method);
var data = method.GetCustomAttribute<MethodDataAttribute>();
Assert.Equal(expectedId, data.Data);
}
public static TheoryData FindSyncMethod_ReturnsMethodMatchingParametersData
{
get
{
var derivedClass = new DerivedAgain();
return new TheoryData<Type, object[], string>
{
{ typeof(ViewComponentWithSyncInvoke), new object[] { }, "1" },
{ typeof(ViewComponentWithSyncInvoke), new object[] { 2, 3 }, "2" },
{ typeof(ViewComponentWithSyncInvoke), new object[] { "", 0, true }, "3" },
{ typeof(MethodsWithValueConversions), new object[] { 1, (byte)2, 3.0f }, "1" },
{ typeof(MethodsWithValueConversions), new object[] { derivedClass, derivedClass }, "3" },
{ typeof(MethodsWithValueConversions), new object[] { "Hello world" }, "5" },
{ typeof(DerivedClass), new object[] { }, "Derived1" },
#if !DNXCORE50
{ typeof(DerivedAgain), new object[] { "" }, "Derived2" },
#endif
};
}
}
[Theory]
[MemberData(nameof(FindSyncMethod_ReturnsMethodMatchingParametersData))]
public void FindSyncMethod_ReturnsMethodMatchingParameters(Type type, object[] args, string expectedId)
{
// Arrange
var typeInfo = type.GetTypeInfo();
// Act
var method = ViewComponentMethodSelector.FindSyncMethod(typeInfo, args);
// Assert
Assert.NotNull(method);
var data = method.GetCustomAttribute<MethodDataAttribute>();
Assert.Equal(expectedId, data.Data);
}
private class ViewComponentWithSyncInvoke
{
[MethodData("1")]
public int Invoke() => 3;
[MethodData("2")]
public int Invoke(int a, int? b) => a + b.Value;
[MethodData("3")]
public int Invoke(string a, int b, bool? c) => 3;
}
private class ViewComponentWithAsyncInvoke
{
[MethodData("1")]
public Task<string> InvokeAsync(string value) => Task.FromResult(value.ToUpperInvariant());
[MethodData("2")]
public Task<string> InvokeAsync(string a, int b) => Task.FromResult(a + b);
[MethodData("3")]
public Task<string> InvokeAsync(string a, int? b, int c) => Task.FromResult(a + b + c);
[MethodData("4")]
public Task<string> InvokeAsync(int? a, bool? b, int c) => Task.FromResult(a.ToString() + b + c);
[MethodData("5")]
public Task<string> InvokeAsync(object value) => Task.FromResult(value.ToString());
}
public class MethodsWithValueConversions
{
[MethodData("1")]
public int Invoke(long a, char b, double c) => 1;
[MethodData("2")]
public Task<int> InvokeAsync(float a, float b, byte c) => Task.FromResult(1);
[MethodData("3")]
public int Invoke(BaseClass a, DerivedClass b) => 1;
[MethodData("4")]
public Task<int> InvokeAsync(BaseClass a, DerivedClass b) => Task.FromResult(1);
[MethodData("5")]
public int Invoke(IEnumerable<char> value) => 1;
[MethodData("6")]
public Task<int> InvokeAsync(IFormatProvider formatProvider) => Task.FromResult(1);
}
private class ViewComponentWithNonPublicNonInstanceInvokes
{
public static int Invoke() => 1;
private int Invoke(string a) => 2;
public static Task<int> InvokeAsync() => Task.FromResult(3);
protected Task<string> InvokeAsync(string a) => Task.FromResult(a);
}
public class BaseClass
{
[MethodData("Base")]
public static int Invoke() => 1;
}
public class DerivedClass : BaseClass
{
[MethodData("Derived1")]
public new int Invoke() => 1;
[MethodData("Derived2")]
public int Invoke(string x) => 2;
}
public class DerivedAgain : DerivedClass
{
[MethodData("DerivedAgain")]
public new static int Invoke(string x) => 2;
}
private class TypeWithInvalidInvokeAsync
{
public Task InvokeAsync(string value) => Task.FromResult(value);
public void InvokeAsync(int value)
{
}
public long InvokeAsync(string a, int b) => b;
}
private class TypeWithInvalidInvokeSync
{
public Task Invoke(string value) => Task.FromResult(value);
public void Invoke(int value)
{
}
}
private class MethodDataAttribute : Attribute
{
public MethodDataAttribute(string data)
{
Data = data;
}
public string Data { get; }
}
}
}