From 63d96255369dff110cfc2f1a3bc4cccbf56016b4 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 10 Jul 2014 19:37:36 -0700 Subject: [PATCH] [Issue #730] Attribute Routing: Flesh out attributes - Part 2 1. Unsealed the Http*Attributes so that they can be extended and customized. 2. Added the same constructors as HttpGet to the rest of the Http*Attributes. 3. Added unit tests to validate the implementations for the IActionHttpMethodProvider. 4. Added functional tests to cover extra attribute routing scenarios like a test for an action with an HttpDeleteAttribute on it and action with AcceptVerbsAttribute and an action with a custom HttpMergeAttribute implemented. --- .../HttpDeleteAttribute.cs | 26 +++++++- .../HttpGetAttribute.cs | 2 +- .../HttpPatchAttribute.cs | 26 +++++++- .../HttpPostAttribute.cs | 27 +++++++- .../HttpPutAttribute.cs | 26 +++++++- .../HttpMethodProviderAttributesTests.cs | 37 +++++++++++ .../RoutingTests.cs | 61 +++++++++++++++++-- .../Controllers/EmployeeController.cs | 28 +++++++-- .../RoutingWebSite/HttpMergeAttribute.cs | 29 +++++++++ .../RoutingWebSite/RoutingWebSite.kproj | 1 + 10 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/HttpMethodProviderAttributesTests.cs create mode 100644 test/WebSites/RoutingWebSite/HttpMergeAttribute.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs index 58793de99d..6a1fb69183 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpDeleteAttribute.cs @@ -3,17 +3,41 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Mvc.Routing; namespace Microsoft.AspNet.Mvc { + /// + /// Identifies an action that only supports the HTTP DELETE method. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public sealed class HttpDeleteAttribute : Attribute, IActionHttpMethodProvider + public class HttpDeleteAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider { private static readonly IEnumerable _supportedMethods = new string[] { "DELETE" }; + /// + /// Creates a new . + /// + public HttpDeleteAttribute() + { + } + + /// + /// Creates a new with the given route template. + /// + /// The route template. May not be null. + public HttpDeleteAttribute([NotNull] string template) + { + Template = template; + } + + /// public IEnumerable HttpMethods { get { return _supportedMethods; } } + + /// + public string Template { get; private 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 index 7b37883259..72c3145108 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpGetAttribute.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc /// Identifies an action that only supports the HTTP GET method. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public sealed class HttpGetAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider + public class HttpGetAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider { private static readonly IEnumerable _supportedMethods = new string[] { "GET" }; diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs index 97e6f91600..4710ff29b7 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpPatchAttribute.cs @@ -3,17 +3,41 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Mvc.Routing; namespace Microsoft.AspNet.Mvc { + /// + /// Identifies an action that only supports the HTTP PATCH method. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public sealed class HttpPatchAttribute : Attribute, IActionHttpMethodProvider + public class HttpPatchAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider { private static readonly IEnumerable _supportedMethods = new string[] { "PATCH" }; + /// + /// Creates a new . + /// + public HttpPatchAttribute() + { + } + + /// + /// Creates a new with the given route template. + /// + /// The route template. May not be null. + public HttpPatchAttribute([NotNull] string template) + { + Template = template; + } + + /// public IEnumerable HttpMethods { get { return _supportedMethods; } } + + /// + public string Template { get; private set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs index 14cc912b02..5a771992d4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpPostAttribute.cs @@ -3,17 +3,42 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Mvc.Routing; namespace Microsoft.AspNet.Mvc { + /// + /// Identifies an action that only supports the HTTP POST method. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public sealed class HttpPostAttribute : Attribute, IActionHttpMethodProvider + public class HttpPostAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider { private static readonly IEnumerable _supportedMethods = new string[] { "POST" }; + /// + /// Creates a new . + /// + /// The route template. May not be null. + public HttpPostAttribute() + { + } + + /// + /// Creates a new with the given route template. + /// + /// The route template. May not be null. + public HttpPostAttribute([NotNull] string template) + { + Template = template; + } + + /// public IEnumerable HttpMethods { get { return _supportedMethods; } } + + /// + public string Template { get; private set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs b/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs index 37e1632250..9ef79a1bb4 100644 --- a/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.Core/HttpPutAttribute.cs @@ -3,17 +3,41 @@ using System; using System.Collections.Generic; +using Microsoft.AspNet.Mvc.Routing; namespace Microsoft.AspNet.Mvc { + /// + /// Identifies an action that only supports the HTTP PUT method. + /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public sealed class HttpPutAttribute : Attribute, IActionHttpMethodProvider + public class HttpPutAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider { private static readonly IEnumerable _supportedMethods = new string[] { "PUT" }; + /// + /// Creates a new . + /// + public HttpPutAttribute() + { + } + + /// + /// Creates a new with the given route template. + /// + /// The route template. May not be null. + public HttpPutAttribute([NotNull] string template) + { + Template = template; + } + + /// public IEnumerable HttpMethods { get { return _supportedMethods; } } + + /// + public string Template { get; private set; } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/HttpMethodProviderAttributesTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/HttpMethodProviderAttributesTests.cs new file mode 100644 index 0000000000..a0ef8cfe52 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/HttpMethodProviderAttributesTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class HttpMethodProviderAttributesTests + { + [Theory] + [MemberData("HttpMethodProviderTestData")] + public void HttpMethodProviderAttributes_ReturnsCorrectHttpMethodSequence( + IActionHttpMethodProvider httpMethodProvider, + IEnumerable expectedHttpMethods) + { + // Act & Assert + Assert.Equal(expectedHttpMethods, httpMethodProvider.HttpMethods); + } + + public static TheoryData> HttpMethodProviderTestData + { + get + { + var data = new TheoryData>(); + data.Add(new HttpGetAttribute(), new[] { "GET" }); + data.Add(new HttpPostAttribute(), new[] { "POST" }); + data.Add(new HttpPutAttribute(), new[] { "PUT" }); + data.Add(new HttpPatchAttribute(), new[] { "PATCH" }); + data.Add(new HttpDeleteAttribute(), new[] { "DELETE" }); + data.Add(new AcceptVerbsAttribute("MERGE", "OPTIONS"), new[] { "MERGE", "OPTIONS" }); + + return data; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs index bec2033ccf..e6495a539a 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RoutingTests.cs @@ -260,16 +260,18 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("List", result.Action); } - // There's an [HttpGet] with its own template on the action here. - [Fact] - public async Task AttributeRoutedAction_ControllerLevelRoute_CombinedWithActionRoute_IsReachable() + // There's no [HttpGet] on the action here. + [Theory] + [InlineData("PUT")] + [InlineData("PATCH")] + public async Task AttributeRoutedAction_ControllerLevelRoute_WithAcceptVerbs_IsReachable(string verb) { // Arrange var server = TestServer.Create(_services, _app); var client = server.Handler; // Act - var response = await client.GetAsync("http://localhost/api/Employee/5/Boss"); + var response = await client.SendAsync(verb, "http://localhost/api/Employee"); // Assert Assert.Equal(200, response.StatusCode); @@ -277,9 +279,56 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var body = await response.ReadBodyAsStringAsync(); var result = JsonConvert.DeserializeObject(body); - Assert.Contains("/api/Employee/5/Boss", result.ExpectedUrls); + // Assert + Assert.Contains("/api/Employee", result.ExpectedUrls); Assert.Equal("Employee", result.Controller); - Assert.Equal("GetBoss", result.Action); + Assert.Equal("UpdateEmployee", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_WithCustomHttpAttributes_IsReachable() + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.Handler; + + // Act + var response = await client.SendAsync("MERGE", "http://localhost/api/Employee/5"); + + // Assert + Assert.Equal(200, response.StatusCode); + + var body = await response.ReadBodyAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + // Assert + Assert.Contains("/api/Employee/5", result.ExpectedUrls); + Assert.Equal("Employee", result.Controller); + Assert.Equal("MergeEmployee", result.Action); + } + + // There's an [HttpGet] with its own template on the action here. + [Theory] + [InlineData("GET", "GetAdministrator")] + [InlineData("DELETE", "DeleteAdministrator")] + public async Task AttributeRoutedAction_ControllerLevelRoute_CombinedWithActionRoute_IsReachable(string verb, string action) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.Handler; + + // Act + var response = await client.SendAsync(verb, "http://localhost/api/Employee/5/Administrator"); + + // Assert + Assert.Equal(200, response.StatusCode); + + var body = await response.ReadBodyAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains("/api/Employee/5/Administrator", result.ExpectedUrls); + Assert.Equal("Employee", result.Controller); + Assert.Equal(action, result.Action); Assert.Contains( new KeyValuePair("id", "5"), diff --git a/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs b/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs index 2c3a1ffdba..9c9b796ccc 100644 --- a/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/EmployeeController.cs @@ -1,5 +1,5 @@ -using Microsoft.AspNet.Mvc; -using System; +using System; +using Microsoft.AspNet.Mvc; namespace RoutingWebSite { @@ -20,16 +20,28 @@ namespace RoutingWebSite return _generator.Generate("/api/Employee"); } + [AcceptVerbs("PUT", "PATCH")] + public IActionResult UpdateEmployee() + { + return _generator.Generate("/api/Employee"); + } + + [HttpMerge("{id}")] + public IActionResult MergeEmployee(int id) + { + return _generator.Generate("/api/Employee/" + id); + } + [HttpGet("{id}")] public IActionResult Get(int id) { return _generator.Generate("/api/Employee/" + id); } - [HttpGet("{id}/Boss")] - public IActionResult GetBoss(int id) + [HttpGet("{id}/Administrator")] + public IActionResult GetAdministrator(int id) { - return _generator.Generate("/api/Employee/" + id + "/Boss"); + return _generator.Generate("/api/Employee/" + id + "/Administrator"); } [HttpGet("~/Manager/{id}")] @@ -37,5 +49,11 @@ namespace RoutingWebSite { return _generator.Generate("/Manager/" + id); } + + [HttpDelete("{id}/Administrator")] + public IActionResult DeleteAdministrator(int id) + { + return _generator.Generate("/api/Employee/" + id + "/Administrator"); + } } } \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/HttpMergeAttribute.cs b/test/WebSites/RoutingWebSite/HttpMergeAttribute.cs new file mode 100644 index 0000000000..9c10c7118a --- /dev/null +++ b/test/WebSites/RoutingWebSite/HttpMergeAttribute.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.Routing; +using Microsoft.AspNet.Routing; + +namespace RoutingWebSite +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class HttpMergeAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider + { + private static readonly IEnumerable _supportedMethods = new[] { "MERGE" }; + + public HttpMergeAttribute(string template) + { + Template = template; + } + + public IEnumerable HttpMethods + { + get { return _supportedMethods; } + } + + public string Template { get; private set; } + } +} \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/RoutingWebSite.kproj b/test/WebSites/RoutingWebSite/RoutingWebSite.kproj index 8557aedcb0..8eef3bf9bd 100644 --- a/test/WebSites/RoutingWebSite/RoutingWebSite.kproj +++ b/test/WebSites/RoutingWebSite/RoutingWebSite.kproj @@ -36,6 +36,7 @@ +