diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionNameAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ActionNameAttribute.cs new file mode 100644 index 0000000000..bc6d1376c9 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ActionNameAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Microsoft.AspNet.Mvc +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ActionNameAttribute : Attribute + { + public ActionNameAttribute(string name) + { + Name = name; + } + + public string Name { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs index 531058e7d4..f9b45204aa 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs @@ -104,38 +104,43 @@ namespace Microsoft.AspNet.Mvc private bool HasCustomAttributes(MethodInfo methodInfo) { var actionAttributes = GetActionCustomAttributes(methodInfo); - return actionAttributes.HttpMethodProviderAttributes.Any(); + return actionAttributes.Any(); } private ActionAttributes GetActionCustomAttributes(MethodInfo methodInfo) { - var httpMethodConstraints = methodInfo.GetCustomAttributes().OfType(); + var attributes = methodInfo.GetCustomAttributes(); + var actionNameAttribute = attributes.OfType().FirstOrDefault(); + var httpMethodConstraints = attributes.OfType(); return new ActionAttributes() { - HttpMethodProviderAttributes = httpMethodConstraints + HttpMethodProviderAttributes = httpMethodConstraints, + ActionNameAttribute = actionNameAttribute }; } private IEnumerable GetActionsForMethodsWithCustomAttributes(MethodInfo methodInfo) { - var httpMethodConstraints = GetActionCustomAttributes(methodInfo).HttpMethodProviderAttributes; - if (!httpMethodConstraints.Any()) + var actionAttributes = GetActionCustomAttributes(methodInfo); + if (!actionAttributes.Any()) { + // If the action is not decorated with any of the attributes, + // it would be handled by convention. yield break; } - var httpMethods = httpMethodConstraints.SelectMany(x => x.HttpMethods).Distinct().ToArray(); - if (httpMethods.Any()) + var actionNameAttribute = actionAttributes.ActionNameAttribute; + var actionName = actionNameAttribute != null ? actionNameAttribute.Name : methodInfo.Name; + + var httpMethodProviders = actionAttributes.HttpMethodProviderAttributes; + var httpMethods = httpMethodProviders.SelectMany(x => x.HttpMethods).Distinct().ToArray(); + + yield return new ActionInfo() { - // Any method which does not follow convention and does not have - // an explicit NoAction attribute is exposed as a method with action name. - yield return new ActionInfo() - { - HttpMethods = httpMethods, - ActionName = methodInfo.Name, - RequireActionNameMatch = true - }; - } + HttpMethods = httpMethods, + ActionName = actionName, + RequireActionNameMatch = true + }; } private IEnumerable GetActionsForMethodsWithoutCustomAttributes(MethodInfo methodInfo, TypeInfo controllerTypeInfo) @@ -201,7 +206,13 @@ namespace Microsoft.AspNet.Mvc private class ActionAttributes { - public IEnumerable HttpMethodProviderAttributes { get; set; } + public IEnumerable HttpMethodProviderAttributes { get; set; } + public ActionNameAttribute ActionNameAttribute { get; set; } + + public bool Any() + { + return ActionNameAttribute != null || HttpMethodProviderAttributes.Any(); + } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs index 268f0acb9c..97b0fefef2 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs @@ -108,6 +108,64 @@ namespace Microsoft.AspNet.Mvc.Core.Test Assert.Equal("Index", result.Name); } + [Theory] + [InlineData("GET")] + [InlineData("PUT")] + [InlineData("POST")] + [InlineData("DELETE")] + [InlineData("PATCH")] + public async Task ActionNameAttribute_ActionGetsExposedViaActionName_UnreachableByConvention(string verb) + { + // Arrange + var requestContext = new RequestContext( + GetHttpContext(verb), + new Dictionary + { + { "controller", "ActionName" }, + { "action", "RPCMethodWithHttpGet" } + }); + + // Act + var result = await InvokeActionSelector(requestContext); + + // Assert + Assert.Equal(null, result); + } + + [Theory] + [InlineData("GET", "CustomActionName_Verb")] + [InlineData("PUT", "CustomActionName_Verb")] + [InlineData("POST", "CustomActionName_Verb")] + [InlineData("DELETE", "CustomActionName_Verb")] + [InlineData("PATCH", "CustomActionName_Verb")] + [InlineData("GET", "CustomActionName_DefaultMethod")] + [InlineData("PUT", "CustomActionName_DefaultMethod")] + [InlineData("POST", "CustomActionName_DefaultMethod")] + [InlineData("DELETE", "CustomActionName_DefaultMethod")] + [InlineData("PATCH", "CustomActionName_DefaultMethod")] + [InlineData("GET", "CustomActionName_RpcMethod")] + [InlineData("PUT", "CustomActionName_RpcMethod")] + [InlineData("POST", "CustomActionName_RpcMethod")] + [InlineData("DELETE", "CustomActionName_RpcMethod")] + [InlineData("PATCH", "CustomActionName_RpcMethod")] + public async Task ActionNameAttribute_DifferentActionName_UsesActionNameFromActionNameAttribute(string verb, string actionName) + { + // Arrange + var requestContext = new RequestContext( + GetHttpContext(verb), + new Dictionary + { + { "controller", "ActionName" }, + { "action", actionName } + }); + + // Act + var result = await InvokeActionSelector(requestContext); + + // Assert + Assert.Equal(actionName, result.Name); + } + private async Task InvokeActionSelector(RequestContext context) { return await InvokeActionSelector(context, _actionDiscoveryConventions); @@ -178,6 +236,24 @@ namespace Microsoft.AspNet.Mvc.Core.Test { } } + private class ActionNameController + { + [ActionName("CustomActionName_Verb")] + public void Put() + { + } + + [ActionName("CustomActionName_DefaultMethod")] + public void Index() + { + } + + [ActionName("CustomActionName_RpcMethod")] + public void RPCMethodWithHttpGet() + { + } + } + private class HttpMethodAttributeTests_RestOnlyController { [HttpGet]