Implement RouteKeyHandling.CatchAll
This commit is contained in:
parent
8bfb6eb8d5
commit
b72b44c20c
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -120,4 +120,4 @@
|
|||
<Compile Include="ViewResultTest.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue