[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.
This commit is contained in:
Javier Calvarro Nelson 2014-07-10 19:37:36 -07:00
parent 21b1174d76
commit 63d9625536
10 changed files with 247 additions and 16 deletions

View File

@ -3,17 +3,41 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Identifies an action that only supports the HTTP DELETE method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class HttpDeleteAttribute : Attribute, IActionHttpMethodProvider
public class HttpDeleteAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
{
private static readonly IEnumerable<string> _supportedMethods = new string[] { "DELETE" };
/// <summary>
/// Creates a new <see cref="HttpDeleteAttribute"/>.
/// </summary>
public HttpDeleteAttribute()
{
}
/// <summary>
/// Creates a new <see cref="HttpDeleteAttribute"/> with the given route template.
/// </summary>
/// <param name="template">The route template. May not be null.</param>
public HttpDeleteAttribute([NotNull] string template)
{
Template = template;
}
/// <inheritdoc />
public IEnumerable<string> HttpMethods
{
get { return _supportedMethods; }
}
/// <inheritdoc />
public string Template { get; private set; }
}
}

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Mvc
/// Identifies an action that only supports the HTTP GET method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class HttpGetAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
public class HttpGetAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
{
private static readonly IEnumerable<string> _supportedMethods = new string[] { "GET" };

View File

@ -3,17 +3,41 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Identifies an action that only supports the HTTP PATCH method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class HttpPatchAttribute : Attribute, IActionHttpMethodProvider
public class HttpPatchAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
{
private static readonly IEnumerable<string> _supportedMethods = new string[] { "PATCH" };
/// <summary>
/// Creates a new <see cref="HttpPatchAttribute"/>.
/// </summary>
public HttpPatchAttribute()
{
}
/// <summary>
/// Creates a new <see cref="HttpPatchAttribute"/> with the given route template.
/// </summary>
/// <param name="template">The route template. May not be null.</param>
public HttpPatchAttribute([NotNull] string template)
{
Template = template;
}
/// <inheritdoc />
public IEnumerable<string> HttpMethods
{
get { return _supportedMethods; }
}
/// <inheritdoc />
public string Template { get; private set; }
}
}

View File

@ -3,17 +3,42 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Identifies an action that only supports the HTTP POST method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class HttpPostAttribute : Attribute, IActionHttpMethodProvider
public class HttpPostAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
{
private static readonly IEnumerable<string> _supportedMethods = new string[] { "POST" };
/// <summary>
/// Creates a new <see cref="HttpPostAttribute"/>.
/// </summary>
/// <param name="template">The route template. May not be null.</param>
public HttpPostAttribute()
{
}
/// <summary>
/// Creates a new <see cref="HttpPostAttribute"/> with the given route template.
/// </summary>
/// <param name="template">The route template. May not be null.</param>
public HttpPostAttribute([NotNull] string template)
{
Template = template;
}
/// <inheritdoc />
public IEnumerable<string> HttpMethods
{
get { return _supportedMethods; }
}
/// <inheritdoc />
public string Template { get; private set; }
}
}

View File

@ -3,17 +3,41 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Identifies an action that only supports the HTTP PUT method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class HttpPutAttribute : Attribute, IActionHttpMethodProvider
public class HttpPutAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
{
private static readonly IEnumerable<string> _supportedMethods = new string[] { "PUT" };
/// <summary>
/// Creates a new <see cref="HttpPutAttribute"/>.
/// </summary>
public HttpPutAttribute()
{
}
/// <summary>
/// Creates a new <see cref="HttpPutAttribute"/> with the given route template.
/// </summary>
/// <param name="template">The route template. May not be null.</param>
public HttpPutAttribute([NotNull] string template)
{
Template = template;
}
/// <inheritdoc />
public IEnumerable<string> HttpMethods
{
get { return _supportedMethods; }
}
/// <inheritdoc />
public string Template { get; private set; }
}
}

View File

@ -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<string> expectedHttpMethods)
{
// Act & Assert
Assert.Equal(expectedHttpMethods, httpMethodProvider.HttpMethods);
}
public static TheoryData<IActionHttpMethodProvider, IEnumerable<string>> HttpMethodProviderTestData
{
get
{
var data = new TheoryData<IActionHttpMethodProvider, IEnumerable<string>>();
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;
}
}
}
}

View File

@ -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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(body);
Assert.Contains("/api/Employee/5/Administrator", result.ExpectedUrls);
Assert.Equal("Employee", result.Controller);
Assert.Equal(action, result.Action);
Assert.Contains(
new KeyValuePair<string, object>("id", "5"),

View File

@ -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");
}
}
}

View File

@ -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<string> _supportedMethods = new[] { "MERGE" };
public HttpMergeAttribute(string template)
{
Template = template;
}
public IEnumerable<string> HttpMethods
{
get { return _supportedMethods; }
}
public string Template { get; private set; }
}
}

View File

@ -36,6 +36,7 @@
<Compile Include="Controllers\EmployeeController.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Controllers\StoreController.cs" />
<Compile Include="HttpMergeAttribute.cs" />
<Compile Include="Startup.cs" />
<Compile Include="TestResponseGenerator.cs" />
</ItemGroup>