Action Selection Convention: Changes for Treating Index method as a default Get/Post method.
Also adds unit tests for the changes.
This commit is contained in:
parent
c72c80c101
commit
8d4a6c760a
|
|
@ -1,4 +1,6 @@
|
||||||
namespace Microsoft.AspNet.Mvc
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
public class ActionInfo
|
public class ActionInfo
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,34 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
public class DefaultActionDiscoveryConventions : IActionDiscoveryConventions
|
public class DefaultActionDiscoveryConventions : IActionDiscoveryConventions
|
||||||
{
|
{
|
||||||
private static readonly string[] _supportedHttpMethodsByConvention =
|
private static readonly string[] _supportedHttpMethodsByConvention =
|
||||||
{
|
{
|
||||||
"GET",
|
"GET",
|
||||||
"POST",
|
"POST",
|
||||||
"PUT",
|
"PUT",
|
||||||
"DELETE",
|
"DELETE",
|
||||||
"PATCH",
|
"PATCH",
|
||||||
};
|
};
|
||||||
|
|
||||||
public virtual bool IsController(TypeInfo typeInfo)
|
private static readonly string[] _supportedHttpMethodsForDefaultMethod =
|
||||||
{
|
{
|
||||||
if (typeInfo == null)
|
"GET",
|
||||||
{
|
"POST"
|
||||||
throw new ArgumentNullException("typeInfo");
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
public virtual string DefaultMethodName
|
||||||
|
{
|
||||||
|
get { return "Index"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool IsController([NotNull] TypeInfo typeInfo)
|
||||||
|
{
|
||||||
if (!typeInfo.IsClass ||
|
if (!typeInfo.IsClass ||
|
||||||
typeInfo.IsAbstract ||
|
typeInfo.IsAbstract ||
|
||||||
typeInfo.ContainsGenericParameters)
|
typeInfo.ContainsGenericParameters)
|
||||||
|
|
@ -37,46 +43,82 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
return typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ||
|
return typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ||
|
||||||
typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo);
|
typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<ActionInfo> GetActions(MethodInfo methodInfo)
|
// If the convention is All methods starting with Get do not have an action name,
|
||||||
|
// for a input GetXYZ methodInfo, the return value will be
|
||||||
|
// { { HttpMethods = "GET", ActionName = "GetXYZ", RequireActionNameMatch = false }}
|
||||||
|
public virtual IEnumerable<ActionInfo> GetActions(
|
||||||
|
[NotNull] MethodInfo methodInfo,
|
||||||
|
[NotNull] TypeInfo controllerTypeInfo)
|
||||||
{
|
{
|
||||||
if (methodInfo == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("methodInfo");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsValidMethod(methodInfo))
|
if (!IsValidMethod(methodInfo))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < _supportedHttpMethodsByConvention.Length; i++)
|
var actionInfos = new List<ActionInfo>();
|
||||||
|
var httpMethods = GetSupportedHttpMethods(methodInfo);
|
||||||
|
if (httpMethods != null && httpMethods.Any())
|
||||||
{
|
{
|
||||||
if (methodInfo.Name.Equals(_supportedHttpMethodsByConvention[i], StringComparison.OrdinalIgnoreCase))
|
return new[] {
|
||||||
{
|
|
||||||
return new [] {
|
|
||||||
new ActionInfo()
|
new ActionInfo()
|
||||||
{
|
{
|
||||||
HttpMethods = new[] { _supportedHttpMethodsByConvention[i] },
|
HttpMethods = httpMethods.ToArray(),
|
||||||
ActionName = methodInfo.Name,
|
ActionName = methodInfo.Name,
|
||||||
RequireActionNameMatch = false,
|
RequireActionNameMatch = false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Default Method add an action Info with GET, POST Http Method constraints.
|
||||||
|
// Only constraints (out of GET and POST) for which there are no convention based actions available are added.
|
||||||
|
// If there are existing action infos with http constraints for GET and POST, this action info is not added for default method.
|
||||||
|
if (IsDefaultMethod(methodInfo))
|
||||||
|
{
|
||||||
|
var existingHttpMethods = new HashSet<string>();
|
||||||
|
foreach (var validMethodName in controllerTypeInfo.DeclaredMethods)
|
||||||
|
{
|
||||||
|
if (!IsValidMethod(validMethodName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var methodNames = GetSupportedHttpMethods(validMethodName);
|
||||||
|
if (methodNames != null )
|
||||||
|
{
|
||||||
|
existingHttpMethods.UnionWith(methodNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var undefinedHttpMethods = _supportedHttpMethodsForDefaultMethod.Except(
|
||||||
|
existingHttpMethods,
|
||||||
|
StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
if (undefinedHttpMethods.Any())
|
||||||
|
{
|
||||||
|
actionInfos.Add(new ActionInfo()
|
||||||
|
{
|
||||||
|
HttpMethods = undefinedHttpMethods,
|
||||||
|
ActionName = methodInfo.Name,
|
||||||
|
RequireActionNameMatch = false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Consider mapping Index here to both Get and also to Index
|
actionInfos.Add(
|
||||||
|
|
||||||
return new[]
|
|
||||||
{
|
|
||||||
new ActionInfo()
|
new ActionInfo()
|
||||||
{
|
{
|
||||||
ActionName = methodInfo.Name,
|
ActionName = methodInfo.Name,
|
||||||
RequireActionNameMatch = true,
|
RequireActionNameMatch = true,
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
return actionInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool IsDefaultMethod([NotNull] MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
return String.Equals(methodInfo.Name, DefaultMethodName, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool IsValidMethod(MethodInfo method)
|
public virtual bool IsValidMethod(MethodInfo method)
|
||||||
|
|
@ -88,5 +130,17 @@ namespace Microsoft.AspNet.Mvc
|
||||||
!method.IsGenericMethod &&
|
!method.IsGenericMethod &&
|
||||||
!method.IsSpecialName;
|
!method.IsSpecialName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual IEnumerable<string> GetSupportedHttpMethods(MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
var ret =
|
||||||
|
_supportedHttpMethodsByConvention.FirstOrDefault(
|
||||||
|
t => methodInfo.Name.Equals(t, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (ret != null)
|
||||||
|
{
|
||||||
|
yield return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,6 @@ namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
bool IsController(TypeInfo typeInfo);
|
bool IsController(TypeInfo typeInfo);
|
||||||
|
|
||||||
IEnumerable<ActionInfo> GetActions(MethodInfo methodInfo);
|
IEnumerable<ActionInfo> GetActions(MethodInfo methodInfo, TypeInfo controllerTypeInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,7 @@ namespace Microsoft.AspNet.Mvc
|
||||||
|
|
||||||
foreach (var methodInfo in cd.ControllerTypeInfo.DeclaredMethods)
|
foreach (var methodInfo in cd.ControllerTypeInfo.DeclaredMethods)
|
||||||
{
|
{
|
||||||
var actionInfos = _conventions.GetActions(methodInfo);
|
var actionInfos = _conventions.GetActions(methodInfo, cd.ControllerTypeInfo);
|
||||||
|
|
||||||
if (actionInfos == null)
|
if (actionInfos == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -123,4 +122,4 @@ namespace Microsoft.AspNet.Mvc
|
||||||
return ad;
|
return ad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
#if NET45
|
||||||
|
|
||||||
|
using Microsoft.AspNet.Abstractions;
|
||||||
|
using Microsoft.AspNet.DependencyInjection.NestedProviders;
|
||||||
|
using Moq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Core.Test
|
||||||
|
{
|
||||||
|
public class ActionSelectionConventionTests
|
||||||
|
{
|
||||||
|
private IActionDiscoveryConventions _actionDiscoveryConventions = new DefaultActionDiscoveryConventions();
|
||||||
|
private IControllerDescriptorFactory _controllerDescriptorFactory = new DefaultControllerDescriptorFactory();
|
||||||
|
private IParameterDescriptorFactory _parameterDescriptorFactory = new DefaultParameterDescriptorFactory();
|
||||||
|
private IEnumerable<Assembly> _controllerAssemblies = new[] { Assembly.GetExecutingAssembly() };
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("GET")]
|
||||||
|
[InlineData("POST")]
|
||||||
|
public async Task ActionSelection_IndexSelectedByDefaultInAbsenceOfVerbOnlyMethod(string verb)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestContext = new RequestContext(
|
||||||
|
GetHttpContext(verb),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "controller", "RpcOnly" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await InvokeActionSelector(requestContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Index", result.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("GET")]
|
||||||
|
[InlineData("POST")]
|
||||||
|
public async Task ActionSelection_PrefersVerbOnlyMethodOverIndex(string verb)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestContext = new RequestContext(
|
||||||
|
GetHttpContext(verb),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "controller", "MixedRpcAndRest" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await InvokeActionSelector(requestContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(verb, result.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("PUT")]
|
||||||
|
[InlineData("DELETE")]
|
||||||
|
[InlineData("PATCH")]
|
||||||
|
public async Task ActionSelection_IndexNotSelectedByDefaultExceptGetAndPostVerbs(string verb)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestContext = new RequestContext(
|
||||||
|
GetHttpContext(verb),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "controller", "RpcOnly" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await InvokeActionSelector(requestContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("HEAD")]
|
||||||
|
[InlineData("OPTIONS")]
|
||||||
|
public async Task ActionSelection_NoConventionBasedRoutingForHeadAndOptions(string verb)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestContext = new RequestContext(
|
||||||
|
GetHttpContext(verb),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{"controller", "MixedRpcAndRest"},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await InvokeActionSelector(requestContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("HEAD")]
|
||||||
|
[InlineData("OPTIONS")]
|
||||||
|
public async Task ActionSelection_ActionNameBasedRoutingForHeadAndOptions(string verb)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestContext = new RequestContext(
|
||||||
|
GetHttpContext(verb),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "controller", "MixedRpcAndRest" },
|
||||||
|
{ "action", verb },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await InvokeActionSelector(requestContext);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(verb, result.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionSelection_ChangeDefaultConventionPicksCustomMethodForPost_DefaultMethodIsSelectedForGet()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestContext = new RequestContext(
|
||||||
|
GetHttpContext("GET"),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "controller", "RpcOnly" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await InvokeActionSelector(requestContext, new CustomActionConvention());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("INDEX", result.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ActionSelection_ChangeDefaultConventionPicksCustomMethodForPost_CutomMethodIsSelected()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var requestContext = new RequestContext(
|
||||||
|
GetHttpContext("POST"),
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "controller", "RpcOnly" }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await InvokeActionSelector(requestContext, new CustomActionConvention());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("PostSomething", result.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ActionDescriptor> InvokeActionSelector(RequestContext context)
|
||||||
|
{
|
||||||
|
return await InvokeActionSelector(context, _actionDiscoveryConventions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ActionDescriptor> InvokeActionSelector(RequestContext context, IActionDiscoveryConventions actionDiscoveryConventions)
|
||||||
|
{
|
||||||
|
var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
|
||||||
|
var descriptorProvider =
|
||||||
|
new NestedProviderManager<ActionDescriptorProviderContext>(new[] { actionDescriptorProvider });
|
||||||
|
var bindingProvider = new Mock<IActionBindingContextProvider>();
|
||||||
|
|
||||||
|
var defaultActionSelector = new DefaultActionSelector(descriptorProvider, bindingProvider.Object);
|
||||||
|
return await defaultActionSelector.SelectAsync(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReflectedActionDescriptorProvider GetActionDescriptorProvider(IActionDiscoveryConventions actionDiscoveryConventions)
|
||||||
|
{
|
||||||
|
var controllerAssemblyProvider = new Mock<IControllerAssemblyProvider>();
|
||||||
|
controllerAssemblyProvider.SetupGet(x => x.CandidateAssemblies).Returns(_controllerAssemblies);
|
||||||
|
return new ReflectedActionDescriptorProvider(
|
||||||
|
controllerAssemblyProvider.Object,
|
||||||
|
actionDiscoveryConventions,
|
||||||
|
_controllerDescriptorFactory,
|
||||||
|
_parameterDescriptorFactory,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpContext GetHttpContext(string httpMethod)
|
||||||
|
{
|
||||||
|
var request = new Mock<HttpRequest>();
|
||||||
|
var headers = new Mock<IHeaderDictionary>();
|
||||||
|
request.SetupGet(r => r.Headers).Returns(headers.Object);
|
||||||
|
request.SetupGet(x => x.Method).Returns(httpMethod);
|
||||||
|
|
||||||
|
var httpContext = new Mock<HttpContext>();
|
||||||
|
httpContext.SetupGet(c => c.Request).Returns(request.Object);
|
||||||
|
return httpContext.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomActionConvention : DefaultActionDiscoveryConventions
|
||||||
|
{
|
||||||
|
public override IEnumerable<string> GetSupportedHttpMethods(MethodInfo methodInfo)
|
||||||
|
{
|
||||||
|
if (methodInfo.Name.Equals("PostSomething", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new[] { "POST" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Controller Classes
|
||||||
|
|
||||||
|
private class MixedRpcAndRestController
|
||||||
|
{
|
||||||
|
public void Index()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Get()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Post()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public void GetSomething()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
// This will be treated as an RPC method.
|
||||||
|
public void Head()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will be treated as an RPC method.
|
||||||
|
public void Options()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RestOnlyController
|
||||||
|
{
|
||||||
|
public void Get()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Put()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Post()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Patch()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RpcOnlyController
|
||||||
|
{
|
||||||
|
public void Index()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetSomething()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PutSomething()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PostSomething()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteSomething()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PatchSomething()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AmbiguousController
|
||||||
|
{
|
||||||
|
public void Index(int i)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public void Index(string s)
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion Controller Classes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
"Microsoft.AspNet.Testing": "0.1-alpha-*",
|
"Microsoft.AspNet.Testing": "0.1-alpha-*",
|
||||||
"Microsoft.AspNet.Mvc.ModelBinding": "",
|
"Microsoft.AspNet.Mvc.ModelBinding": "",
|
||||||
"Microsoft.AspNet.Mvc.Rendering": "",
|
"Microsoft.AspNet.Mvc.Rendering": "",
|
||||||
|
"Microsoft.AspNet.DependencyInjection" : "0.1-alpha-*",
|
||||||
|
"Microsoft.AspNet.Abstractions" : "0.1-alpha-*",
|
||||||
|
"Microsoft.AspNet.Mvc.ModelBinding" : "",
|
||||||
"Moq": "4.2.1312.1622",
|
"Moq": "4.2.1312.1622",
|
||||||
"Xunit.KRunner": "0.1-alpha-*",
|
"Xunit.KRunner": "0.1-alpha-*",
|
||||||
"xunit.abstractions": "2.0.0-aspnet-*",
|
"xunit.abstractions": "2.0.0-aspnet-*",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue