Implement RouteKeyHandling.CatchAll

This commit is contained in:
Ryan Nowak 2014-07-14 15:41:56 -07:00
parent 8bfb6eb8d5
commit b72b44c20c
13 changed files with 383 additions and 8 deletions

View File

@ -1226,6 +1226,22 @@ namespace Microsoft.AspNet.Mvc.Core
return GetString("AttributeRoute_TokenReplacement_UnescapedBraceInToken");
}
/// <summary>
/// The value must be either '{0}' or '{1}'.
/// </summary>
internal static string RouteConstraintAttribute_InvalidKeyHandlingValue
{
get { return GetString("RouteConstraintAttribute_InvalidKeyHandlingValue"); }
}
/// <summary>
/// The value must be either '{0}' or '{1}'.
/// </summary>
internal static string FormatRouteConstraintAttribute_InvalidKeyHandlingValue(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RouteConstraintAttribute_InvalidKeyHandlingValue"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -215,9 +215,18 @@ namespace Microsoft.AspNet.Mvc
// Skip duplicates
if (!HasConstraint(actionDescriptor.RouteConstraints, constraintAttribute.RouteKey))
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteValue));
if (constraintAttribute.RouteValue == null)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteKeyHandling));
}
else
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
constraintAttribute.RouteKey,
constraintAttribute.RouteValue));
}
}
}
@ -229,7 +238,13 @@ namespace Microsoft.AspNet.Mvc
// want to provide these values as ambient values.
foreach (var constraint in actionDescriptor.RouteConstraints)
{
actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
// We don't need to do anything with attribute routing for 'catch all' behavior. Order
// and predecedence of attribute routes allow this kind of behavior.
if (constraint.KeyHandling == RouteKeyHandling.RequireKey ||
constraint.KeyHandling == RouteKeyHandling.DenyKey)
{
actionDescriptor.RouteValueDefaults.Add(constraint.RouteKey, constraint.RouteValue);
}
}
// Replaces tokens like [controller]/[action] in the route template with the actual values

View File

@ -348,4 +348,7 @@
<data name="AttributeRoute_TokenReplacement_UnescapedBraceInToken" xml:space="preserve">
<value>An unescaped '[' token is not allowed inside of a replacement token. Use '[[' to escape.</value>
</data>
<data name="RouteConstraintAttribute_InvalidKeyHandlingValue" xml:space="preserve">
<value>The value must be either '{0}' or '{1}'.</value>
</data>
</root>

View File

@ -2,12 +2,59 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// An attribute which specifies a required route value for an action or controller.
///
/// When placed on an action, the route data of a request must match the expectations of the route
/// constraint in order for the action to be selected. See <see cref="RouteKeyHandling"/> for
/// the expectations that must be satisfied by the route data.
///
/// When placed on a controller, unless overridden by the action, the constraint applies to all
/// actions defined by the controller.
///
///
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class RouteConstraintAttribute : Attribute
{
/// <summary>
/// Creates a new <see cref="RouteConstraintAttribute"/>.
/// </summary>
/// <param name="routeKey">The route value key.</param>
/// <param name="keyHandling">
/// The <see cref="RouteKeyHandling"/> value. Must be <see cref="RouteKeyHandling.CatchAll "/>
/// or <see cref="RouteKeyHandling.DenyKey"/>.
/// </param>
protected RouteConstraintAttribute(
[NotNull] string routeKey,
RouteKeyHandling keyHandling)
{
RouteKey = routeKey;
RouteKeyHandling = keyHandling;
if (keyHandling != RouteKeyHandling.CatchAll &&
keyHandling != RouteKeyHandling.DenyKey)
{
var message = Resources.FormatRouteConstraintAttribute_InvalidKeyHandlingValue(
Enum.GetName(typeof(RouteKeyHandling), RouteKeyHandling.CatchAll),
Enum.GetName(typeof(RouteKeyHandling), RouteKeyHandling.DenyKey));
throw new ArgumentException(message, "keyHandling");
}
}
/// <summary>
/// Creates a new <see cref="RouteConstraintAttribute"/> with
/// <see cref="RouteConstraintAttribute.RouteKeyHandling"/> set to <see cref="RouteKeyHandling.RequireKey"/>.
/// </summary>
/// <param name="routeKey">The route value key.</param>
/// <param name="routeValue">The expected route value.</param>
/// <param name="blockNonAttributedActions">
/// Set to true to negate this constraint on all actions that do not define a behavior for this route key.
/// </param>
protected RouteConstraintAttribute(
[NotNull]string routeKey,
[NotNull]string routeValue,
@ -18,8 +65,25 @@ namespace Microsoft.AspNet.Mvc
BlockNonAttributedActions = blockNonAttributedActions;
}
/// <summary>
/// The route value key.
/// </summary>
public string RouteKey { get; private set; }
/// <summary>
/// The <see cref="RouteKeyHandling"/>.
/// </summary>
public RouteKeyHandling RouteKeyHandling { get; private set; }
/// <summary>
/// The expected route value. Will be null unless <see cref="RouteConstraintAttribute.RouteKeyHandling"/> is
/// set to <see cref="RouteKeyHandling.RequireKey"/>.
/// </summary>
public string RouteValue { get; private set; }
/// <summary>
/// Set to true to negate this constraint on all actions that do not define a behavior for this route key.
/// </summary>
public bool BlockNonAttributedActions { get; private set; }
}
}

View File

@ -176,6 +176,92 @@ namespace Microsoft.AspNet.Mvc
Assert.Same(action, actionWithConstraints);
}
[Fact]
public async Task SelectAsync_WithCatchAll_PrefersNonCatchAll()
{
// Arrange
var actions = new ActionDescriptor[]
{
CreateAction(area: null, controller: "Store", action: "Buy"),
CreateAction(area: null, controller: "Store", action: "Buy"),
CreateAction(area: null, controller: "Store", action: "Buy"),
};
actions[0].RouteConstraints.Add(new RouteDataActionConstraint("country", "CA"));
actions[1].RouteConstraints.Add(new RouteDataActionConstraint("country", "US"));
actions[2].RouteConstraints.Add(new RouteDataActionConstraint("country", RouteKeyHandling.CatchAll));
var selector = CreateSelector(actions);
var context = CreateRouteContext("GET");
context.RouteData.Values.Add("controller", "Store");
context.RouteData.Values.Add("action", "Buy");
context.RouteData.Values.Add("country", "CA");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, actions[0]);
}
[Fact]
public async Task SelectAsync_WithCatchAll_CatchAllIsOnlyMatch()
{
// Arrange
var actions = new ActionDescriptor[]
{
CreateAction(area: null, controller: "Store", action: "Buy"),
CreateAction(area: null, controller: "Store", action: "Buy"),
CreateAction(area: null, controller: "Store", action: "Buy"),
};
actions[0].RouteConstraints.Add(new RouteDataActionConstraint("country", "CA"));
actions[1].RouteConstraints.Add(new RouteDataActionConstraint("country", "US"));
actions[2].RouteConstraints.Add(new RouteDataActionConstraint("country", RouteKeyHandling.CatchAll));
var selector = CreateSelector(actions);
var context = CreateRouteContext("GET");
context.RouteData.Values.Add("controller", "Store");
context.RouteData.Values.Add("action", "Buy");
context.RouteData.Values.Add("country", "DE");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Same(action, actions[2]);
}
[Fact]
public async Task SelectAsync_WithCatchAll_NoMatch()
{
// Arrange
var actions = new ActionDescriptor[]
{
CreateAction(area: null, controller: "Store", action: "Buy"),
CreateAction(area: null, controller: "Store", action: "Buy"),
CreateAction(area: null, controller: "Store", action: "Buy"),
};
actions[0].RouteConstraints.Add(new RouteDataActionConstraint("country", "CA"));
actions[1].RouteConstraints.Add(new RouteDataActionConstraint("country", "US"));
actions[2].RouteConstraints.Add(new RouteDataActionConstraint("country", RouteKeyHandling.CatchAll));
var selector = CreateSelector(actions);
var context = CreateRouteContext("GET");
context.RouteData.Values.Add("controller", "Store");
context.RouteData.Values.Add("action", "Buy");
// Act
var action = await selector.SelectAsync(context);
// Assert
Assert.Null(action);
}
private static ActionDescriptor[] GetActions()
{
return new ActionDescriptor[]
@ -268,6 +354,7 @@ namespace Microsoft.AspNet.Mvc
{
Name = string.Format("Area: {0}, Controller: {1}, Action: {2}", area, controller, action),
RouteConstraints = new List<RouteDataActionConstraint>(),
Parameters = new List<ParameterDescriptor>(),
};
actionDescriptor.RouteConstraints.Add(

View File

@ -120,4 +120,4 @@
<Compile Include="ViewResultTest.cs" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>
</Project>

View File

@ -817,6 +817,110 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal("/Admin/Users/All", result.Link);
}
[Fact]
public async Task ControllerWithCatchAll_CanReachSpecificCountry()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var response = await client.GetAsync("http://localhost/api/Products/US/GetProducts");
// Assert
Assert.Equal(200, response.StatusCode);
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
Assert.Contains("/api/Products/US/GetProducts", result.ExpectedUrls);
Assert.Equal("Products", result.Controller);
Assert.Equal("GetProducts", result.Action);
Assert.Equal(
new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "country", "US" },
{ "action", "GetProducts" },
{ "controller", "Products" },
},
result.RouteValues);
}
// The 'default' route doesn't provide a value for {country}
[Fact]
public async Task ControllerWithCatchAll_CannotReachWithoutCountry()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var response = await client.GetAsync("http://localhost/Products/GetProducts");
// Assert
Assert.Equal(404, response.StatusCode);
}
[Fact]
public async Task ControllerWithCatchAll_GenerateLinkForSpecificCountry()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var url =
LinkFrom("http://localhost/")
.To(new { action = "GetProducts", controller = "Products", country = "US" });
var response = await client.GetAsync(url);
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
// Assert
Assert.Equal("/api/Products/US/GetProducts", result.Link);
}
[Fact]
public async Task ControllerWithCatchAll_GenerateLinkForFallback()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var url =
LinkFrom("http://localhost/")
.To(new { action = "GetProducts", controller = "Products", country = "CA" });
var response = await client.GetAsync(url);
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
// Assert
Assert.Equal("/api/Products/CA/GetProducts", result.Link);
}
[Fact]
public async Task ControllerWithCatchAll_GenerateLink_FailsWithoutCountry()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var url =
LinkFrom("http://localhost/")
.To(new { action = "GetProducts", controller = "Products", country = (string)null });
var response = await client.GetAsync(url);
var body = await response.ReadBodyAsStringAsync();
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
// Assert
Assert.Null(result.Link);
}
private static LinkBuilder LinkFrom(string url)
{
return new LinkBuilder(url);

View File

@ -0,0 +1,23 @@
// 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 Microsoft.AspNet.Mvc;
namespace RoutingWebSite.Products
{
[CountryNeutral]
public class ProductsController : Controller
{
private readonly TestResponseGenerator _generator;
public ProductsController(TestResponseGenerator generator)
{
_generator = generator;
}
public IActionResult GetProducts()
{
return _generator.Generate("/api/Products/CA/GetProducts");
}
}
}

View File

@ -0,0 +1,23 @@
// 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 Microsoft.AspNet.Mvc;
namespace RoutingWebSite.Products.US
{
[CountrySpecific("US")]
public class ProductsController : Controller
{
private readonly TestResponseGenerator _generator;
public ProductsController(TestResponseGenerator generator)
{
_generator = generator;
}
public IActionResult GetProducts()
{
return _generator.Generate("/api/Products/US/GetProducts");
}
}
}

View File

@ -0,0 +1,15 @@
// 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 Microsoft.AspNet.Mvc;
namespace RoutingWebSite
{
public class CountryNeutralAttribute : RouteConstraintAttribute
{
public CountryNeutralAttribute()
: base("country", RouteKeyHandling.CatchAll)
{
}
}
}

View File

@ -0,0 +1,15 @@
// 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 Microsoft.AspNet.Mvc;
namespace RoutingWebSite
{
public class CountrySpecificAttribute : RouteConstraintAttribute
{
public CountrySpecificAttribute(string countryCode)
: base("country", countryCode, blockNonAttributedActions: false)
{
}
}
}

View File

@ -35,9 +35,13 @@
<Compile Include="Controllers\BlogController.cs" />
<Compile Include="Controllers\EmployeeController.cs" />
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Controllers\Products\ProductsController.cs" />
<Compile Include="Controllers\Products\US\ProductsController.cs" />
<Compile Include="Controllers\StoreController.cs" />
<Compile Include="Controllers\TeamController.cs" />
<Compile Include="HttpMergeAttribute.cs" />
<Compile Include="CountryNeutralAttribute.cs" />
<Compile Include="CountrySpecificAttribute.cs" />
<Compile Include="Startup.cs" />
<Compile Include="TestResponseGenerator.cs" />
</ItemGroup>

View File

@ -1,7 +1,8 @@
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc;
// 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 Microsoft.AspNet.Builder;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.DependencyInjection;
namespace RoutingWebSite
@ -27,6 +28,11 @@ namespace RoutingWebSite
routes.MapRoute("ActionAsMethod", "{controller}/{action}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
"products",
"api/Products/{country}/{action}",
defaults: new { controller = "Products" });
});
}
}