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:
harshgMSFT 2014-03-25 14:35:01 -07:00
parent c72c80c101
commit 8d4a6c760a
6 changed files with 398 additions and 36 deletions

View File

@ -1,4 +1,6 @@
namespace Microsoft.AspNet.Mvc
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc
{
public class ActionInfo
{

View File

@ -1,28 +1,34 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
{
public class DefaultActionDiscoveryConventions : IActionDiscoveryConventions
{
private static readonly string[] _supportedHttpMethodsByConvention =
{
"GET",
"POST",
"PUT",
"DELETE",
private static readonly string[] _supportedHttpMethodsByConvention =
{
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
};
public virtual bool IsController(TypeInfo typeInfo)
private static readonly string[] _supportedHttpMethodsForDefaultMethod =
{
if (typeInfo == null)
{
throw new ArgumentNullException("typeInfo");
}
"GET",
"POST"
};
public virtual string DefaultMethodName
{
get { return "Index"; }
}
public virtual bool IsController([NotNull] TypeInfo typeInfo)
{
if (!typeInfo.IsClass ||
typeInfo.IsAbstract ||
typeInfo.ContainsGenericParameters)
@ -37,46 +43,82 @@ namespace Microsoft.AspNet.Mvc
return typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) ||
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))
{
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()
{
HttpMethods = new[] { _supportedHttpMethodsByConvention[i] },
HttpMethods = httpMethods.ToArray(),
ActionName = methodInfo.Name,
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
return new[]
{
actionInfos.Add(
new ActionInfo()
{
ActionName = methodInfo.Name,
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)
@ -88,5 +130,17 @@ namespace Microsoft.AspNet.Mvc
!method.IsGenericMethod &&
!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;
}
}
}
}
}

View File

@ -7,6 +7,6 @@ namespace Microsoft.AspNet.Mvc
{
bool IsController(TypeInfo typeInfo);
IEnumerable<ActionInfo> GetActions(MethodInfo methodInfo);
IEnumerable<ActionInfo> GetActions(MethodInfo methodInfo, TypeInfo controllerTypeInfo);
}
}

View File

@ -60,8 +60,7 @@ namespace Microsoft.AspNet.Mvc
foreach (var methodInfo in cd.ControllerTypeInfo.DeclaredMethods)
{
var actionInfos = _conventions.GetActions(methodInfo);
var actionInfos = _conventions.GetActions(methodInfo, cd.ControllerTypeInfo);
if (actionInfos == null)
{
continue;
@ -123,4 +122,4 @@ namespace Microsoft.AspNet.Mvc
return ad;
}
}
}
}

View File

@ -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

View File

@ -6,6 +6,9 @@
"Microsoft.AspNet.Testing": "0.1-alpha-*",
"Microsoft.AspNet.Mvc.ModelBinding": "",
"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",
"Xunit.KRunner": "0.1-alpha-*",
"xunit.abstractions": "2.0.0-aspnet-*",