[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:
parent
21b1174d76
commit
63d9625536
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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" };
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue