Adding HttpGetAttribute and AcceptVerbsAttribute.

HttpPost, HttpDelete, HttpPut and HttpPatch would be similar.
Also adding few tests.
This commit is contained in:
harshgMSFT 2014-03-24 11:06:59 -07:00
parent df16982697
commit 652e89b343
7 changed files with 358 additions and 44 deletions

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Specifies what HTTP methods an action supports.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider
{
private readonly IEnumerable<string> _httpMethods;
/// <summary>
/// Initializes a new instance of the <see cref="AcceptVerbsAttribute" /> class.
/// </summary>
/// <param name="method">The HTTP method the action supports.</param>
public AcceptVerbsAttribute([NotNull] string method)
: this(new string[] { method })
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AcceptVerbsAttribute" /> class.
/// </summary>
/// <param name="methods">The HTTP methods the action supports.</param>
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());
}
/// <summary>
/// Gets the HTTP methods the action supports.
/// </summary>
public IEnumerable<string> HttpMethods
{
get
{
return _httpMethods;
}
}
}
}

View File

@ -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<string> HttpMethods { get; }
}
}

View File

@ -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<string> 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<IActionHttpMethodProvider>();
return new ActionAttributes()
{
HttpMethodProviderAttributes = httpMethodConstraints
};
}
private IEnumerable<ActionInfo> 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<ActionInfo> GetActionsForMethodsWithoutCustomAttributes(MethodInfo methodInfo, TypeInfo controllerTypeInfo)
{
var actionInfos = new List<ActionInfo>();
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<string>();
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<string> GetSupportedHttpMethods(MethodInfo methodInfo)
{
var ret =
_supportedHttpMethodsByConvention.FirstOrDefault(
t => methodInfo.Name.Equals(t, StringComparison.OrdinalIgnoreCase));
if (ret != null)
{
yield return ret;
}
public IEnumerable<IActionHttpMethodProvider> HttpMethodProviderAttributes { get; set; }
}
}
}

View File

@ -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<string> _supportedMethods = new string[] { "GET" };
public IEnumerable<string> HttpMethods
{
get { return _supportedMethods; }
}
}
}

View File

@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Mvc
IEnumerable<IFilter> globalFilters)
{
_controllerAssemblyProvider = controllerAssemblyProvider;
_conventions = conventions;
_conventions = conventions;
_controllerDescriptorFactory = controllerDescriptorFactory;
_parameterDescriptorFactory = parameterDescriptorFactory;
var filters = globalFilters ?? Enumerable.Empty<IFilter>();

View File

@ -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<Assembly> _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<string, object>
{
{ "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<string, object>
{
{ "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<string, object>
{
{ "controller", "HttpMethodAttributeTests_DefaultMethodValidation" },
});
// Act
var result = await InvokeActionSelector(requestContext);
// Assert
Assert.Equal("Index", result.Name);
}
private async Task<ActionDescriptor> InvokeActionSelector(RequestContext context)
{
return await InvokeActionSelector(context, _actionDiscoveryConventions);
}
private async Task<ActionDescriptor> InvokeActionSelector(RequestContext context, DefaultActionDiscoveryConventions 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(DefaultActionDiscoveryConventions 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 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

View File

@ -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<Assembly> _controllerAssemblies = new[] { Assembly.GetExecutingAssembly() };
@ -161,7 +161,7 @@ namespace Microsoft.AspNet.Mvc.Core.Test
return await InvokeActionSelector(context, _actionDiscoveryConventions);
}
private async Task<ActionDescriptor> InvokeActionSelector(RequestContext context, IActionDiscoveryConventions actionDiscoveryConventions)
private async Task<ActionDescriptor> 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<IControllerAssemblyProvider>();
controllerAssemblyProvider.SetupGet(x => x.CandidateAssemblies).Returns(_controllerAssemblies);