diff --git a/src/Microsoft.AspNet.Mvc.Core/AcceptVerbsAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/AcceptVerbsAttribute.cs
new file mode 100644
index 0000000000..f86fdfa985
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/AcceptVerbsAttribute.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.AspNet.Mvc
+{
+ ///
+ /// Specifies what HTTP methods an action supports.
+ ///
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private readonly IEnumerable _httpMethods;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The HTTP method the action supports.
+ public AcceptVerbsAttribute([NotNull] string method)
+ : this(new string[] { method })
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The HTTP methods the action supports.
+ public AcceptVerbsAttribute(params string[] methods)
+ {
+ // TODO: This assumes that the methods are exactly same as standard Http Methods.
+ // The Http Abstractions should take care of these.
+ _httpMethods = methods.Select(method => method.ToUpperInvariant());
+ }
+
+ ///
+ /// Gets the HTTP methods the action supports.
+ ///
+ public IEnumerable HttpMethods
+ {
+ get
+ {
+ return _httpMethods;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionMethodSelectorAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/ActionMethodSelectorAttribute.cs
new file mode 100644
index 0000000000..c0dfa95770
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/ActionMethodSelectorAttribute.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public interface IActionHttpMethodProvider
+ {
+ IEnumerable HttpMethods { get; }
+ }
+}
\ 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 ccdf85ff6e..531058e7d4 100644
--- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionDiscoveryConventions.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net;
using System.Reflection;
namespace Microsoft.AspNet.Mvc
@@ -52,45 +53,127 @@ namespace Microsoft.AspNet.Mvc
[NotNull] MethodInfo methodInfo,
[NotNull] TypeInfo controllerTypeInfo)
{
- if (!IsValidMethod(methodInfo))
+ if (!IsValidActionMethod(methodInfo))
{
return null;
}
+ var actionInfos = GetActionsForMethodsWithCustomAttributes(methodInfo);
+ if (actionInfos.Any())
+ {
+ return actionInfos;
+ }
+ else
+ {
+ actionInfos = GetActionsForMethodsWithoutCustomAttributes(methodInfo, controllerTypeInfo);
+ }
+
+ return actionInfos;
+ }
+
+ protected virtual bool IsDefaultActionMethod([NotNull] MethodInfo methodInfo)
+ {
+ return String.Equals(methodInfo.Name, DefaultMethodName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ protected virtual bool IsValidActionMethod(MethodInfo method)
+ {
+ return
+ method.IsPublic &&
+ !method.IsAbstract &&
+ !method.IsConstructor &&
+ !method.IsGenericMethod &&
+
+ // The SpecialName bit is set to flag members that are treated in a special way by some compilers
+ // (such as property accessors and operator overloading methods).
+ !method.IsSpecialName;
+ }
+
+ public virtual IEnumerable GetSupportedHttpMethods(MethodInfo methodInfo)
+ {
+ var supportedHttpMethods =
+ _supportedHttpMethodsByConvention.FirstOrDefault(
+ httpMethod => methodInfo.Name.Equals(httpMethod, StringComparison.OrdinalIgnoreCase));
+
+ if (supportedHttpMethods != null)
+ {
+ yield return supportedHttpMethods;
+ }
+ }
+
+ private bool HasCustomAttributes(MethodInfo methodInfo)
+ {
+ var actionAttributes = GetActionCustomAttributes(methodInfo);
+ return actionAttributes.HttpMethodProviderAttributes.Any();
+ }
+
+ private ActionAttributes GetActionCustomAttributes(MethodInfo methodInfo)
+ {
+ var httpMethodConstraints = methodInfo.GetCustomAttributes().OfType();
+ return new ActionAttributes()
+ {
+ HttpMethodProviderAttributes = httpMethodConstraints
+ };
+ }
+
+ private IEnumerable GetActionsForMethodsWithCustomAttributes(MethodInfo methodInfo)
+ {
+ var httpMethodConstraints = GetActionCustomAttributes(methodInfo).HttpMethodProviderAttributes;
+ if (!httpMethodConstraints.Any())
+ {
+ yield break;
+ }
+
+ var httpMethods = httpMethodConstraints.SelectMany(x => x.HttpMethods).Distinct().ToArray();
+ if (httpMethods.Any())
+ {
+ // 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
+ };
+ }
+ }
+
+ private IEnumerable GetActionsForMethodsWithoutCustomAttributes(MethodInfo methodInfo, TypeInfo controllerTypeInfo)
+ {
var actionInfos = new List();
var httpMethods = GetSupportedHttpMethods(methodInfo);
if (httpMethods != null && httpMethods.Any())
{
- return new[] {
- new ActionInfo()
- {
- HttpMethods = httpMethods.ToArray(),
- ActionName = methodInfo.Name,
- RequireActionNameMatch = false,
- }
- };
+ return new[]
+ {
+ new ActionInfo()
+ {
+ 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))
+ if (IsDefaultActionMethod(methodInfo))
{
var existingHttpMethods = new HashSet();
- foreach (var validMethodName in controllerTypeInfo.DeclaredMethods)
+ foreach (var declaredMethodInfo in controllerTypeInfo.DeclaredMethods)
{
- if (!IsValidMethod(validMethodName))
+ if (!IsValidActionMethod(declaredMethodInfo) || HasCustomAttributes(declaredMethodInfo))
{
continue;
}
- var methodNames = GetSupportedHttpMethods(validMethodName);
- if (methodNames != null )
+ httpMethods = GetSupportedHttpMethods(declaredMethodInfo);
+ if (httpMethods != null)
{
- existingHttpMethods.UnionWith(methodNames);
+ existingHttpMethods.UnionWith(httpMethods);
}
}
-
var undefinedHttpMethods = _supportedHttpMethodsForDefaultMethod.Except(
existingHttpMethods,
StringComparer.Ordinal)
@@ -116,31 +199,9 @@ namespace Microsoft.AspNet.Mvc
return actionInfos;
}
- public virtual bool IsDefaultMethod([NotNull] MethodInfo methodInfo)
+ private class ActionAttributes
{
- return String.Equals(methodInfo.Name, DefaultMethodName, StringComparison.OrdinalIgnoreCase);
- }
-
- public virtual bool IsValidMethod(MethodInfo method)
- {
- return
- method.IsPublic &&
- !method.IsAbstract &&
- !method.IsConstructor &&
- !method.IsGenericMethod &&
- !method.IsSpecialName;
- }
-
- public virtual IEnumerable GetSupportedHttpMethods(MethodInfo methodInfo)
- {
- var ret =
- _supportedHttpMethodsByConvention.FirstOrDefault(
- t => methodInfo.Name.Equals(t, StringComparison.OrdinalIgnoreCase));
-
- if (ret != null)
- {
- yield return ret;
- }
+ public IEnumerable HttpMethodProviderAttributes { get; set; }
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs
new file mode 100644
index 0000000000..4252ae40d6
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNet.Mvc
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public sealed class HttpGetAttribute : Attribute, IActionHttpMethodProvider
+ {
+ private static readonly IEnumerable _supportedMethods = new string[] { "GET" };
+
+ public IEnumerable HttpMethods
+ {
+ get { return _supportedMethods; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
index 0e25e2baf8..0cf4c2bcda 100644
--- a/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
@@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc
IEnumerable globalFilters)
{
_controllerAssemblyProvider = controllerAssemblyProvider;
- _conventions = conventions;
+ _conventions = conventions;
_controllerDescriptorFactory = controllerDescriptorFactory;
_parameterDescriptorFactory = parameterDescriptorFactory;
var filters = globalFilters ?? Enumerable.Empty();
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs
new file mode 100644
index 0000000000..d23cc7312c
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionAttributeTests.cs
@@ -0,0 +1,180 @@
+#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 ActionAttributeTests
+ {
+ private DefaultActionDiscoveryConventions _actionDiscoveryConventions = new DefaultActionDiscoveryConventions();
+ private IControllerDescriptorFactory _controllerDescriptorFactory = new DefaultControllerDescriptorFactory();
+ private IParameterDescriptorFactory _parameterDescriptorFactory = new DefaultParameterDescriptorFactory();
+ private IEnumerable _controllerAssemblies = new[] { Assembly.GetExecutingAssembly() };
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PUT")]
+ [InlineData("POST")]
+ public async Task HttpMethodAttribute_ActionDecoratedWithMultipleHttpMethodAttribute_ORsMultipleHttpMethods(string verb)
+ {
+ // Arrange
+ var requestContext = new RequestContext(
+ GetHttpContext(verb),
+ new Dictionary
+ {
+ { "controller", "HttpMethodAttributeTests_RestOnly" },
+ { "action", "Put" }
+ });
+
+ // Act
+ var result = await InvokeActionSelector(requestContext);
+
+ // Assert
+ Assert.Equal("Put", result.Name);
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("PUT")]
+ public async Task HttpMethodAttribute_ActionDecoratedWithHttpMethodAttribute_OverridesConvention(string verb)
+ {
+ // Arrange
+ // Note no action name is passed, hence should return a null action descriptor.
+ var requestContext = new RequestContext(
+ GetHttpContext(verb),
+ new Dictionary
+ {
+ { "controller", "HttpMethodAttributeTests_RestOnly" },
+ });
+
+ // Act
+ var result = await InvokeActionSelector(requestContext);
+
+ // Assert
+ Assert.Equal(null, result);
+ }
+
+ [Theory]
+ [InlineData("GET")]
+ [InlineData("POST")]
+ public async Task HttpMethodAttribute_DefaultMethod_IgnoresMethodsWithCustomAttributesAndInvalidMethods(string verb)
+ {
+ // Arrange
+ // Note no action name is passed, hence should return a null action descriptor.
+ var requestContext = new RequestContext(
+ GetHttpContext(verb),
+ new Dictionary
+ {
+ { "controller", "HttpMethodAttributeTests_DefaultMethodValidation" },
+ });
+
+ // Act
+ var result = await InvokeActionSelector(requestContext);
+
+ // Assert
+ Assert.Equal("Index", result.Name);
+ }
+
+ private async Task InvokeActionSelector(RequestContext context)
+ {
+ return await InvokeActionSelector(context, _actionDiscoveryConventions);
+ }
+
+ private async Task InvokeActionSelector(RequestContext context, DefaultActionDiscoveryConventions actionDiscoveryConventions)
+ {
+ var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
+ var descriptorProvider =
+ new NestedProviderManager(new[] { actionDescriptorProvider });
+ var bindingProvider = new Mock();
+
+ var defaultActionSelector = new DefaultActionSelector(descriptorProvider, bindingProvider.Object);
+ return await defaultActionSelector.SelectAsync(context);
+ }
+
+ private ReflectedActionDescriptorProvider GetActionDescriptorProvider(DefaultActionDiscoveryConventions actionDiscoveryConventions)
+ {
+ var controllerAssemblyProvider = new Mock();
+ 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();
+ var headers = new Mock();
+ request.SetupGet(r => r.Headers).Returns(headers.Object);
+ request.SetupGet(x => x.Method).Returns(httpMethod);
+ var httpContext = new Mock();
+ httpContext.SetupGet(c => c.Request).Returns(request.Object);
+ return httpContext.Object;
+ }
+
+ private class CustomActionConvention : DefaultActionDiscoveryConventions
+ {
+ public override IEnumerable GetSupportedHttpMethods(MethodInfo methodInfo)
+ {
+ if (methodInfo.Name.Equals("PostSomething", StringComparison.OrdinalIgnoreCase))
+ {
+ return new[] { "POST" };
+ }
+
+ return null;
+ }
+ }
+
+ #region Controller Classes
+
+ private class HttpMethodAttributeTests_DefaultMethodValidationController
+ {
+ public void Index()
+ {
+ }
+
+ // Method with custom attribute.
+ [HttpGet]
+ public void Get()
+ { }
+
+ // InvalidMethod ( since its private)
+ private void Post()
+ { }
+ }
+
+ private class HttpMethodAttributeTests_RestOnlyController
+ {
+ [HttpGet]
+ [AcceptVerbs("PUT", "POST")]
+ public void Put()
+ {
+ }
+
+ public void Delete()
+ {
+ }
+
+ public void Patch()
+ {
+ }
+ }
+
+ private class HttpMethodAttributeTests_DerivedController : HttpMethodAttributeTests_RestOnlyController
+ {
+ }
+
+ #endregion Controller Classes
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs
index 05df84c664..6a7eb0fb6d 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionSelectionConventionTests.cs
@@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
{
public class ActionSelectionConventionTests
{
- private IActionDiscoveryConventions _actionDiscoveryConventions = new DefaultActionDiscoveryConventions();
+ private DefaultActionDiscoveryConventions _actionDiscoveryConventions = new DefaultActionDiscoveryConventions();
private IControllerDescriptorFactory _controllerDescriptorFactory = new DefaultControllerDescriptorFactory();
private IParameterDescriptorFactory _parameterDescriptorFactory = new DefaultParameterDescriptorFactory();
private IEnumerable _controllerAssemblies = new[] { Assembly.GetExecutingAssembly() };
@@ -161,7 +161,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
return await InvokeActionSelector(context, _actionDiscoveryConventions);
}
- private async Task InvokeActionSelector(RequestContext context, IActionDiscoveryConventions actionDiscoveryConventions)
+ private async Task InvokeActionSelector(RequestContext context, DefaultActionDiscoveryConventions actionDiscoveryConventions)
{
var actionDescriptorProvider = GetActionDescriptorProvider(actionDiscoveryConventions);
var descriptorProvider =
@@ -172,7 +172,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
return await defaultActionSelector.SelectAsync(context);
}
- private ReflectedActionDescriptorProvider GetActionDescriptorProvider(IActionDiscoveryConventions actionDiscoveryConventions)
+ private ReflectedActionDescriptorProvider GetActionDescriptorProvider(DefaultActionDiscoveryConventions actionDiscoveryConventions)
{
var controllerAssemblyProvider = new Mock();
controllerAssemblyProvider.SetupGet(x => x.CandidateAssemblies).Returns(_controllerAssemblies);