parent
d9e40d92de
commit
30f5826380
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue