[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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that only supports the HTTP DELETE method.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[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" };
|
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
|
public IEnumerable<string> HttpMethods
|
||||||
{
|
{
|
||||||
get { return _supportedMethods; }
|
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.
|
/// Identifies an action that only supports the HTTP GET method.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[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" };
|
private static readonly IEnumerable<string> _supportedMethods = new string[] { "GET" };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,41 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that only supports the HTTP PATCH method.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[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" };
|
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
|
public IEnumerable<string> HttpMethods
|
||||||
{
|
{
|
||||||
get { return _supportedMethods; }
|
get { return _supportedMethods; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Template { get; private set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,17 +3,42 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that only supports the HTTP POST method.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[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" };
|
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
|
public IEnumerable<string> HttpMethods
|
||||||
{
|
{
|
||||||
get { return _supportedMethods; }
|
get { return _supportedMethods; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Template { get; private set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,17 +3,41 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that only supports the HTTP PUT method.
|
||||||
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
[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" };
|
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
|
public IEnumerable<string> HttpMethods
|
||||||
{
|
{
|
||||||
get { return _supportedMethods; }
|
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);
|
Assert.Equal("List", result.Action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// There's an [HttpGet] with its own template on the action here.
|
// There's no [HttpGet] on the action here.
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task AttributeRoutedAction_ControllerLevelRoute_CombinedWithActionRoute_IsReachable()
|
[InlineData("PUT")]
|
||||||
|
[InlineData("PATCH")]
|
||||||
|
public async Task AttributeRoutedAction_ControllerLevelRoute_WithAcceptVerbs_IsReachable(string verb)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var server = TestServer.Create(_services, _app);
|
var server = TestServer.Create(_services, _app);
|
||||||
var client = server.Handler;
|
var client = server.Handler;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var response = await client.GetAsync("http://localhost/api/Employee/5/Boss");
|
var response = await client.SendAsync(verb, "http://localhost/api/Employee");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(200, response.StatusCode);
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
@ -277,9 +279,56 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
var body = await response.ReadBodyAsStringAsync();
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
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("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(
|
Assert.Contains(
|
||||||
new KeyValuePair<string, object>("id", "5"),
|
new KeyValuePair<string, object>("id", "5"),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Microsoft.AspNet.Mvc;
|
using System;
|
||||||
using System;
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
namespace RoutingWebSite
|
namespace RoutingWebSite
|
||||||
{
|
{
|
||||||
|
|
@ -20,16 +20,28 @@ namespace RoutingWebSite
|
||||||
return _generator.Generate("/api/Employee");
|
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}")]
|
[HttpGet("{id}")]
|
||||||
public IActionResult Get(int id)
|
public IActionResult Get(int id)
|
||||||
{
|
{
|
||||||
return _generator.Generate("/api/Employee/" + id);
|
return _generator.Generate("/api/Employee/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/Boss")]
|
[HttpGet("{id}/Administrator")]
|
||||||
public IActionResult GetBoss(int id)
|
public IActionResult GetAdministrator(int id)
|
||||||
{
|
{
|
||||||
return _generator.Generate("/api/Employee/" + id + "/Boss");
|
return _generator.Generate("/api/Employee/" + id + "/Administrator");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("~/Manager/{id}")]
|
[HttpGet("~/Manager/{id}")]
|
||||||
|
|
@ -37,5 +49,11 @@ namespace RoutingWebSite
|
||||||
{
|
{
|
||||||
return _generator.Generate("/Manager/" + id);
|
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\EmployeeController.cs" />
|
||||||
<Compile Include="Controllers\HomeController.cs" />
|
<Compile Include="Controllers\HomeController.cs" />
|
||||||
<Compile Include="Controllers\StoreController.cs" />
|
<Compile Include="Controllers\StoreController.cs" />
|
||||||
|
<Compile Include="HttpMergeAttribute.cs" />
|
||||||
<Compile Include="Startup.cs" />
|
<Compile Include="Startup.cs" />
|
||||||
<Compile Include="TestResponseGenerator.cs" />
|
<Compile Include="TestResponseGenerator.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue