[Issue #733] Attribute Routing: Implement Order
1. Added an Order property to IRouteTemplateProvider, ReflectedAttributeRouteModel, AttributeRouteInfo, AttributeRouteLinkGenerationEntry and AttributeRouteMatchingEntry. 2. Changed the implementation of AttributeRoute to take the order into account when routing incomming requests and generating links. 3. Ensured a stable ordering of route entries with the same order and precedence for route matching and link generation based on the template text. 4. Added tests to validate that the precedence gets respected in route matching and link generation. 5. Added tests to validate that the order gets respected in route matching and link generation. 6. Added tests to validate that the order gets respected over the precedence for route matching and link generation. 7. Added tests to validate that routes with the same order and precedence expose a stable ordering for route matching and link generation.
This commit is contained in:
parent
63d9625536
commit
3ab0c3af29
|
|
@ -126,6 +126,37 @@ namespace MvcSample.Web
|
|||
return user;
|
||||
}
|
||||
|
||||
[HttpGet("/AttributeRouting/{other}", Order = 0)]
|
||||
public string LowerPrecedence(string param)
|
||||
{
|
||||
return "Lower";
|
||||
}
|
||||
|
||||
// Normally this route would be tried before the one above
|
||||
// as it is more explicit (doesn't have a parameter), but
|
||||
// due to the fact that it has a higher order, it will be
|
||||
// tried after the route above.
|
||||
[HttpGet("/AttributeRouting/HigherPrecedence", Order = 1)]
|
||||
public string HigherOrder()
|
||||
{
|
||||
return "Higher";
|
||||
}
|
||||
|
||||
// Both routes have the same template, which would make
|
||||
// them ambiguous, but the order we defined in the routes
|
||||
// disambiguates them.
|
||||
[HttpGet("/AttributeRouting/SameTemplate", Order = 0)]
|
||||
public string SameTemplateHigherOrderPrecedence()
|
||||
{
|
||||
return "HigherOrderPrecedence";
|
||||
}
|
||||
|
||||
[HttpGet("/AttributeRouting/SameTemplate", Order = 1)]
|
||||
public string SameTemplateLowerOrderPrecedence()
|
||||
{
|
||||
return "LowerOrderPrecedence";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action that exercises default view names.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,13 @@
|
|||
|
||||
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 class HttpDeleteAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
public class HttpDeleteAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new string[] { "DELETE" };
|
||||
|
||||
|
|
@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// Creates a new <see cref="HttpDeleteAttribute"/>.
|
||||
/// </summary>
|
||||
public HttpDeleteAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpDeleteAttribute([NotNull] string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
Template = template;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get { return _supportedMethods; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,13 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies an action that only supports the HTTP GET method.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public class HttpGetAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
public class HttpGetAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new string[] { "GET" };
|
||||
|
||||
|
|
@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// Creates a new <see cref="HttpGetAttribute"/>.
|
||||
/// </summary>
|
||||
public HttpGetAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpGetAttribute([NotNull] string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
Template = template;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get { return _supportedMethods; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// 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.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies an action that only supports a given set of HTTP methods.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
|
||||
public abstract class HttpMethodAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
{
|
||||
private int? _order;
|
||||
private readonly IEnumerable<string> _httpMethods;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HttpMethodAttribute"/> with the given
|
||||
/// set of HTTP methods.
|
||||
/// <param name="httpMethods">The set of supported HTTP methods.</param>
|
||||
/// </summary>
|
||||
public HttpMethodAttribute([NotNull] IEnumerable<string> httpMethods)
|
||||
: this(httpMethods, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="HttpMethodAttribute"/> with the given
|
||||
/// set of HTTP methods an the given route template.
|
||||
/// </summary>
|
||||
/// <param name="httpMethods">The set of supported methods.</param>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpMethodAttribute([NotNull] IEnumerable<string> httpMethods, string template)
|
||||
{
|
||||
_httpMethods = httpMethods;
|
||||
Template = template;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get
|
||||
{
|
||||
return _httpMethods;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route order. The order determines the order of route execution. Routes with a lower
|
||||
/// order value are tried first. When a route doesn't specify a value, it gets the value of the
|
||||
/// <see cref="RouteAttribute.Order"/> or a default value of 0 if the <see cref="RouteAttribute"/>
|
||||
/// doesn't define a value on the controller.
|
||||
/// </summary>
|
||||
public int Order
|
||||
{
|
||||
get { return _order ?? 0; }
|
||||
set { _order = value; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
int? IRouteTemplateProvider.Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return _order;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,13 @@
|
|||
|
||||
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 class HttpPatchAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
public class HttpPatchAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new string[] { "PATCH" };
|
||||
|
||||
|
|
@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// Creates a new <see cref="HttpPatchAttribute"/>.
|
||||
/// </summary>
|
||||
public HttpPatchAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpPatchAttribute([NotNull] string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
Template = template;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get { return _supportedMethods; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,23 +3,21 @@
|
|||
|
||||
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 class HttpPostAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
public class HttpPostAttribute : HttpMethodAttribute
|
||||
{
|
||||
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()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -28,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpPostAttribute([NotNull] string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
Template = template;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get { return _supportedMethods; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,13 @@
|
|||
|
||||
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 class HttpPutAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider
|
||||
public class HttpPutAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new string[] { "PUT" };
|
||||
|
||||
|
|
@ -19,6 +17,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// Creates a new <see cref="HttpPutAttribute"/>.
|
||||
/// </summary>
|
||||
public HttpPutAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -27,17 +26,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpPutAttribute([NotNull] string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
Template = template;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<string> HttpMethods
|
||||
{
|
||||
get { return _supportedMethods; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
<Compile Include="ActionDescriptorsCollection.cs" />
|
||||
<Compile Include="ActionResults\HttpNotFoundResult.cs" />
|
||||
<Compile Include="Formatters\TextPlainFormatter.cs" />
|
||||
<Compile Include="HttpMethodAttribute.cs" />
|
||||
<Compile Include="Logging\AttributeRouteRouteAsyncValues.cs" />
|
||||
<Compile Include="Logging\LoggerExtensions.cs" />
|
||||
<Compile Include="Logging\MvcRouteHandlerRouteAsyncValues.cs" />
|
||||
|
|
@ -190,6 +191,7 @@
|
|||
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedActionModel.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedApplicationModel.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedAttributeRouteModel.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedControllerModel.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedParameterModel.cs" />
|
||||
<Compile Include="Rendering\CompositeViewEngine.cs" />
|
||||
|
|
@ -247,10 +249,10 @@
|
|||
<Compile Include="RouteDataActionConstraint.cs" />
|
||||
<Compile Include="RouteKeyHandling.cs" />
|
||||
<Compile Include="Routing\AttributeRoute.cs" />
|
||||
<Compile Include="Routing\AttributeRouteInfo.cs" />
|
||||
<Compile Include="Routing\AttributeRouteLinkGenerationEntry.cs" />
|
||||
<Compile Include="Routing\AttributeRouteMatchingEntry.cs" />
|
||||
<Compile Include="Routing\AttributeRoutePrecedence.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTemplate.cs" />
|
||||
<Compile Include="Routing\AttributeRouting.cs" />
|
||||
<Compile Include="Routing\IRouteTemplateProvider.cs" />
|
||||
<Compile Include="TemplateInfo.cs" />
|
||||
|
|
|
|||
|
|
@ -17,8 +17,16 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
public class ReflectedActionDescriptorProvider : IActionDescriptorProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the default order associated with this provider for dependency injection
|
||||
/// purposes.
|
||||
/// </summary>
|
||||
public static readonly int DefaultOrder = 0;
|
||||
|
||||
// This is the default order for attribute routes whose order calculated from
|
||||
// the reflected model is null.
|
||||
private const int DefaultAttributeRouteOrder = 0;
|
||||
|
||||
private readonly IControllerAssemblyProvider _controllerAssemblyProvider;
|
||||
private readonly IActionDiscoveryConventions _conventions;
|
||||
private readonly IEnumerable<IFilter> _globalFilters;
|
||||
|
|
@ -114,7 +122,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
var actions = new List<ReflectedActionDescriptor>();
|
||||
|
||||
var routeGroupsByTemplate = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var hasAttributeRoutes = false;
|
||||
var removalConstraints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var routeTemplateErrors = new List<string>();
|
||||
|
|
@ -152,7 +160,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
var attributeRouteInfo = combinedRoute == null ? null : new AttributeRouteInfo()
|
||||
{
|
||||
Template = combinedRoute.Template
|
||||
Template = combinedRoute.Template,
|
||||
Order = combinedRoute.Order ?? DefaultAttributeRouteOrder
|
||||
};
|
||||
|
||||
var actionDescriptor = new ReflectedActionDescriptor()
|
||||
|
|
@ -215,6 +224,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
if (actionDescriptor.AttributeRouteInfo != null &&
|
||||
actionDescriptor.AttributeRouteInfo.Template != null)
|
||||
{
|
||||
hasAttributeRoutes = true;
|
||||
// An attribute routed action will ignore conventional routed constraints. We still
|
||||
// want to provide these values as ambient values.
|
||||
foreach (var constraint in actionDescriptor.RouteConstraints)
|
||||
|
|
@ -243,19 +253,14 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
actionDescriptor.AttributeRouteInfo.Template = templateText;
|
||||
|
||||
// An attribute routed action is matched by its 'route group' which identifies all equivalent
|
||||
// actions.
|
||||
string routeGroup;
|
||||
if (!routeGroupsByTemplate.TryGetValue(templateText, out routeGroup))
|
||||
{
|
||||
routeGroup = GetRouteGroup(templateText);
|
||||
routeGroupsByTemplate.Add(templateText, routeGroup);
|
||||
}
|
||||
var routeGroupValue = GetRouteGroupValue(
|
||||
actionDescriptor.AttributeRouteInfo.Order,
|
||||
templateText);
|
||||
|
||||
var routeConstraints = new List<RouteDataActionConstraint>();
|
||||
routeConstraints.Add(new RouteDataActionConstraint(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
routeGroup));
|
||||
routeGroupValue));
|
||||
|
||||
actionDescriptor.RouteConstraints = routeConstraints;
|
||||
}
|
||||
|
|
@ -279,7 +284,7 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
// Any any attribute routes are in use, then non-attribute-routed ADs can't be selected
|
||||
// when a route group returned by the route.
|
||||
if (routeGroupsByTemplate.Any())
|
||||
if (hasAttributeRoutes)
|
||||
{
|
||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
|
|
@ -320,10 +325,10 @@ namespace Microsoft.AspNet.Mvc
|
|||
return actions;
|
||||
}
|
||||
|
||||
// Returns a unique, stable key per-route-template (OrdinalIgnoreCase)
|
||||
private static string GetRouteGroup(string template)
|
||||
private static string GetRouteGroupValue(int order, string template)
|
||||
{
|
||||
return ("__route__" + template).ToUpperInvariant();
|
||||
var group = string.Format("{0}-{1}", order, template);
|
||||
return ("__route__" + group).ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,17 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
public ReflectedAttributeRouteModel([NotNull] IRouteTemplateProvider templateProvider)
|
||||
{
|
||||
Template = templateProvider.Template;
|
||||
Order = templateProvider.Order;
|
||||
}
|
||||
|
||||
public string Template { get; set; }
|
||||
|
||||
/// <summary>
|
||||
public int? Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Combines two <see cref="ReflectedAttributeRouteModel"/> instances and returns
|
||||
/// a new <see cref="ReflectedAttributeRouteModel"/> instance with the result.
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
/// <param name="left">The left <see cref="ReflectedAttributeRouteModel"/>.</param>
|
||||
/// <param name="right">The right <see cref="ReflectedAttributeRouteModel"/>.</param>
|
||||
/// <returns>A new instance of <see cref="ReflectedAttributeRouteModel"/> that represents the
|
||||
|
|
@ -37,30 +40,31 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
ReflectedAttributeRouteModel left,
|
||||
ReflectedAttributeRouteModel right)
|
||||
{
|
||||
left = left ?? _default;
|
||||
right = right ?? _default;
|
||||
|
||||
var template = CombineTemplates(left.Template, right.Template);
|
||||
// If the right template is an override template (starts with / or ~/)
|
||||
// we ignore the values from left.
|
||||
if (left == null || IsOverridePattern(right.Template))
|
||||
{
|
||||
left = _default;
|
||||
}
|
||||
|
||||
var combinedTemplate = CombineTemplates(left.Template, right.Template);
|
||||
|
||||
// The action is not attribute routed.
|
||||
if (template == null)
|
||||
if (combinedTemplate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ReflectedAttributeRouteModel()
|
||||
{
|
||||
Template = template
|
||||
{
|
||||
Template = combinedTemplate,
|
||||
Order = right.Order ?? left.Order
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines attribute routing templates.
|
||||
/// </summary>
|
||||
/// <param name="left">The left template.</param>
|
||||
/// <param name="right">The right template.</param>
|
||||
/// <returns>A combined template.</returns>
|
||||
public static string CombineTemplates(string left, string right)
|
||||
internal static string CombineTemplates(string left, string right)
|
||||
{
|
||||
var result = CombineCore(left, right);
|
||||
return CleanTemplate(result);
|
||||
|
|
@ -72,19 +76,11 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
{
|
||||
return null;
|
||||
}
|
||||
else if (left == null)
|
||||
{
|
||||
return right;
|
||||
}
|
||||
else if (right == null)
|
||||
{
|
||||
return left;
|
||||
}
|
||||
|
||||
if (right.StartsWith("~/", StringComparison.OrdinalIgnoreCase) ||
|
||||
right.StartsWith("/", StringComparison.OrdinalIgnoreCase) ||
|
||||
left.Equals("~/", StringComparison.OrdinalIgnoreCase) ||
|
||||
left.Equals("/", StringComparison.OrdinalIgnoreCase))
|
||||
else if (IsEmptyLeftSegment(left) || IsOverridePattern(right))
|
||||
{
|
||||
return right;
|
||||
}
|
||||
|
|
@ -98,6 +94,21 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
return left + '/' + right;
|
||||
}
|
||||
|
||||
private static bool IsOverridePattern(string template)
|
||||
{
|
||||
return template != null &&
|
||||
(template.StartsWith("~/", StringComparison.OrdinalIgnoreCase) ||
|
||||
template.StartsWith("/", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsEmptyLeftSegment(string template)
|
||||
{
|
||||
return template == null ||
|
||||
template.Equals(string.Empty, StringComparison.OrdinalIgnoreCase) ||
|
||||
template.Equals("~/", StringComparison.OrdinalIgnoreCase) ||
|
||||
template.Equals("/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string CleanTemplate(string result)
|
||||
{
|
||||
if (result == null)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ namespace Microsoft.AspNet.Mvc
|
|||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public class RouteAttribute : Attribute, IRouteTemplateProvider
|
||||
{
|
||||
private int? _order;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="RouteAttribute"/> with the given route template.
|
||||
/// </summary>
|
||||
|
|
@ -23,5 +25,26 @@ namespace Microsoft.AspNet.Mvc
|
|||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route order. The order determines the order of route execution. Routes with a lower order
|
||||
/// value are tried first. If an action defines a route by providing an <see cref="IRouteTemplateProvider"/>
|
||||
/// with a non <c>null</c> order, that order is used instead of this value. If neither the action nor the
|
||||
/// controller defines an order, a default value of 0 is used.
|
||||
/// </summary>
|
||||
public int Order
|
||||
{
|
||||
get { return _order ?? 0; }
|
||||
set { _order = value; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
int? IRouteTemplateProvider.Order
|
||||
{
|
||||
get
|
||||
{
|
||||
return _order;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,20 +29,30 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// <param name="next">The next router. Invoked when a route entry matches.</param>
|
||||
/// <param name="entries">The set of route entries.</param>
|
||||
public AttributeRoute(
|
||||
[NotNull] IRouter next,
|
||||
[NotNull] IRouter next,
|
||||
[NotNull] IEnumerable<AttributeRouteMatchingEntry> matchingEntries,
|
||||
[NotNull] IEnumerable<AttributeRouteLinkGenerationEntry> linkGenerationEntries,
|
||||
[NotNull] ILoggerFactory factory)
|
||||
{
|
||||
_next = next;
|
||||
|
||||
// FOR RIGHT NOW - this is just an array of regular template routes. We'll follow up by implementing
|
||||
// a good data-structure here. See #740
|
||||
_matchingRoutes = matchingEntries.OrderBy(e => e.Precedence).Select(e => e.Route).ToArray();
|
||||
// Order all the entries by order, then precedence, and then finally by template in order to provide
|
||||
// a stable routing and link generation order for templates with same order and precedence.
|
||||
// We use ordinal comparison for the templates because we only care about them being exactly equal and
|
||||
// we don't want to make any equivalence between templates based on the culture of the machine.
|
||||
|
||||
// FOR RIGHT NOW - this is just an array of entries. We'll follow up by implementing
|
||||
// a good data-structure here. See #741
|
||||
_linkGenerationEntries = linkGenerationEntries.OrderBy(e => e.Precedence).ToArray();
|
||||
_matchingRoutes = matchingEntries
|
||||
.OrderBy(o => o.Order)
|
||||
.ThenBy(e => e.Precedence)
|
||||
.ThenBy(e => e.Route.RouteTemplate, StringComparer.Ordinal)
|
||||
.Select(e => e.Route)
|
||||
.ToArray();
|
||||
|
||||
_linkGenerationEntries = linkGenerationEntries
|
||||
.OrderBy(o => o.Order)
|
||||
.ThenBy(e => e.Precedence)
|
||||
.ThenBy(e => e.TemplateText, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
_logger = factory.Create<AttributeRoute>();
|
||||
_constraintLogger = factory.Create(typeof(RouteConstraintMatcher).FullName);
|
||||
|
|
@ -53,12 +63,12 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
{
|
||||
using (_logger.BeginScope("AttributeRoute.RouteAsync"))
|
||||
{
|
||||
foreach (var route in _matchingRoutes)
|
||||
{
|
||||
await route.RouteAsync(context);
|
||||
foreach (var route in _matchingRoutes)
|
||||
{
|
||||
await route.RouteAsync(context);
|
||||
|
||||
if (context.IsHandled)
|
||||
{
|
||||
if (context.IsHandled)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,12 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// The route template. May be null if the action has no attribute routes.
|
||||
/// </summary>
|
||||
public string Template { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the order of the route associated with this <see cref="ActionDescriptor"/>. This property determines
|
||||
/// the order in which routes get executed. Routes with a lower order value are tried first. In case a route
|
||||
/// doesn't specify a value, it gets a default order of 0.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,11 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// </summary>
|
||||
public IDictionary<string, object> Defaults { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The order of the template.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The precedence of the template.
|
||||
/// </summary>
|
||||
|
|
@ -47,5 +52,10 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// The <see cref="Template"/>.
|
||||
/// </summary>
|
||||
public RouteTemplate Template { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The original <see cref="string"/> representing the route template.
|
||||
/// </summary>
|
||||
public string TemplateText { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,11 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// </summary>
|
||||
public class AttributeRouteMatchingEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The order of the template.
|
||||
/// </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The precedence of the template.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -41,10 +41,12 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults),
|
||||
Defaults = routeInfo.Defaults,
|
||||
Constraints = routeInfo.Constraints,
|
||||
Order = routeInfo.Order,
|
||||
Precedence = routeInfo.Precedence,
|
||||
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
|
||||
RouteGroup = routeInfo.RouteGroup,
|
||||
Template = routeInfo.ParsedTemplate,
|
||||
TemplateText = routeInfo.RouteTemplate
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +59,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
{
|
||||
matchingEntries.Add(new AttributeRouteMatchingEntry()
|
||||
{
|
||||
Order = routeInfo.Order,
|
||||
Precedence = routeInfo.Precedence,
|
||||
Route = new TemplateRoute(
|
||||
target,
|
||||
|
|
@ -72,9 +75,9 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
}
|
||||
|
||||
return new AttributeRoute(
|
||||
target,
|
||||
matchingEntries,
|
||||
generationEntries,
|
||||
target,
|
||||
matchingEntries,
|
||||
generationEntries,
|
||||
services.GetService<ILoggerFactory>());
|
||||
}
|
||||
|
||||
|
|
@ -133,11 +136,11 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
if (errors.Count > 0)
|
||||
{
|
||||
var allErrors = string.Join(
|
||||
Environment.NewLine + Environment.NewLine,
|
||||
Environment.NewLine + Environment.NewLine,
|
||||
errors.Select(
|
||||
e => Resources.FormatAttributeRoute_IndividualErrorMessage(
|
||||
e.ActionDescriptor.DisplayName,
|
||||
Environment.NewLine,
|
||||
e.ActionDescriptor.DisplayName,
|
||||
Environment.NewLine,
|
||||
e.ErrorMessage)));
|
||||
|
||||
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(Environment.NewLine, allErrors);
|
||||
|
|
@ -208,6 +211,8 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
}
|
||||
}
|
||||
|
||||
routeInfo.Order = action.AttributeRouteInfo.Order;
|
||||
|
||||
routeInfo.Precedence = AttributeRoutePrecedence.Compute(routeInfo.ParsedTemplate);
|
||||
|
||||
routeInfo.Constraints = routeInfo.ParsedTemplate.Parameters
|
||||
|
|
@ -233,6 +238,8 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
|
||||
public RouteTemplate ParsedTemplate { get; set; }
|
||||
|
||||
public int Order { get; set; }
|
||||
|
||||
public decimal Precedence { get; set; }
|
||||
|
||||
public string RouteGroup { get; set; }
|
||||
|
|
|
|||
|
|
@ -12,5 +12,13 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// The route template. May be null.
|
||||
/// </summary>
|
||||
string Template { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route order. The order determines the order of route execution. Routes with a lower
|
||||
/// order value are tried first. When a route doesn't specify a value, it gets a default value of 0.
|
||||
/// A null value for the Order property means that the user didn't specify an explicit order for the
|
||||
/// route.
|
||||
/// </summary>
|
||||
int? Order { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
<Compile Include="Formatters\OutputFormatterTests.cs" />
|
||||
<Compile Include="Formatters\TextPlainFormatterTests.cs" />
|
||||
<Compile Include="DefaultViewComponentActivatorTests.cs" />
|
||||
<Compile Include="HttpMethodProviderAttributesTests.cs" />
|
||||
<Compile Include="Logging\BeginScopeContext.cs" />
|
||||
<Compile Include="Logging\TestLoggerFactory.cs" />
|
||||
<Compile Include="Logging\WriteCoreContext.cs" />
|
||||
|
|
@ -88,6 +89,7 @@
|
|||
<Compile Include="ReflectedActionDescriptorProviderTests.cs" />
|
||||
<Compile Include="ReflectedActionInvokerTest.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedActionModelTests.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedAttributeRouteModelTests.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedControllerModelTests.cs" />
|
||||
<Compile Include="ReflectedModelBuilder\ReflectedParameterModelTests.cs" />
|
||||
<Compile Include="Rendering\CompositeViewEngineTest.cs" />
|
||||
|
|
@ -103,6 +105,7 @@
|
|||
<Compile Include="Rendering\ViewContextTests.cs" />
|
||||
<Compile Include="Rendering\ViewDataOfTTest.cs" />
|
||||
<Compile Include="KnownRouteValueConstraintTests.cs" />
|
||||
<Compile Include="RouteTemplateProviderAttributesTests.cs" />
|
||||
<Compile Include="Routing\AttributeRoutePrecedenceTests.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTemplateTests.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTests.cs" />
|
||||
|
|
|
|||
|
|
@ -101,6 +101,210 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
Assert.Equal(expected, combined);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ReplaceTokens_ValueValuesData")]
|
||||
public void ReplaceTokens_ValidValues(string template, object values, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var valuesDictionary = values as IDictionary<string, object>;
|
||||
if (valuesDictionary == null)
|
||||
{
|
||||
valuesDictionary = new RouteValueDictionary(values);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = ReflectedAttributeRouteModel.ReplaceTokens(template, valuesDictionary);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ReplaceTokens_InvalidFormatValuesData")]
|
||||
public void ReplaceTokens_InvalidFormat(string template, object values, string reason)
|
||||
{
|
||||
// Arrange
|
||||
var valuesDictionary = values as IDictionary<string, object>;
|
||||
if (valuesDictionary == null)
|
||||
{
|
||||
valuesDictionary = new RouteValueDictionary(values);
|
||||
}
|
||||
|
||||
var expected = string.Format(
|
||||
"The route template '{0}' has invalid syntax. {1}",
|
||||
template,
|
||||
reason);
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { ReflectedAttributeRouteModel.ReplaceTokens(template, valuesDictionary); });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceTokens_UnknownValue()
|
||||
{
|
||||
// Arrange
|
||||
var template = "[area]/[controller]/[action2]";
|
||||
var values = new RouteValueDictionary()
|
||||
{
|
||||
{ "area", "Help" },
|
||||
{ "controller", "Admin" },
|
||||
{ "action", "SeeUsers" },
|
||||
};
|
||||
|
||||
var expected =
|
||||
"While processing template '[area]/[controller]/[action2]', " +
|
||||
"a replacement value for the token 'action2' could not be found. " +
|
||||
"Available tokens: 'area, controller, action'.";
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { ReflectedAttributeRouteModel.ReplaceTokens(template, values); });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("CombineOrdersTestData")]
|
||||
public void Combine_Orders(
|
||||
ReflectedAttributeRouteModel left,
|
||||
ReflectedAttributeRouteModel right,
|
||||
int? expected)
|
||||
{
|
||||
// Arrange & Act
|
||||
var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(combined);
|
||||
Assert.Equal(expected, combined.Order);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ValidReflectedAttributeRouteModelsTestData")]
|
||||
public void Combine_ValidReflectedAttributeRouteModels(
|
||||
ReflectedAttributeRouteModel left,
|
||||
ReflectedAttributeRouteModel right,
|
||||
ReflectedAttributeRouteModel expectedResult)
|
||||
{
|
||||
// Arrange & Act
|
||||
var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(combined);
|
||||
Assert.Equal(expectedResult.Template, combined.Template);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("NullOrNullTemplateReflectedAttributeRouteModelTestData")]
|
||||
public void Combine_NullOrNullTemplateReflectedAttributeRouteModels(
|
||||
ReflectedAttributeRouteModel left,
|
||||
ReflectedAttributeRouteModel right)
|
||||
{
|
||||
// Arrange & Act
|
||||
var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
|
||||
|
||||
// Assert
|
||||
Assert.Null(combined);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("RightOverridesReflectedAttributeRouteModelTestData")]
|
||||
public void Combine_RightOverridesReflectedAttributeRouteModel(
|
||||
ReflectedAttributeRouteModel left,
|
||||
ReflectedAttributeRouteModel right)
|
||||
{
|
||||
// Arrange
|
||||
var expectedTemplate = ReflectedAttributeRouteModel.CombineTemplates(null, right.Template);
|
||||
|
||||
// Act
|
||||
var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(combined);
|
||||
Assert.Equal(expectedTemplate, combined.Template);
|
||||
Assert.Equal(combined.Order, right.Order);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> CombineOrdersTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel, int?>();
|
||||
|
||||
data.Add(Create("", order: 1), Create("", order: 2), 2);
|
||||
data.Add(Create("", order: 1), Create("", order: null), 1);
|
||||
data.Add(Create("", order: 1), null, 1);
|
||||
data.Add(Create("", order: 1), Create("/", order: 2), 2);
|
||||
data.Add(Create("", order: 1), Create("/", order: null), null);
|
||||
data.Add(Create("", order: null), Create("", order: 2), 2);
|
||||
data.Add(Create("", order: null), Create("", order: null), null);
|
||||
data.Add(Create("", order: null), null, null);
|
||||
data.Add(Create("", order: null), Create("/", order: 2), 2);
|
||||
data.Add(Create("", order: null), Create("/", order: null), null);
|
||||
data.Add(null, Create("", order: 2), 2);
|
||||
data.Add(null, Create("", order: null), null);
|
||||
data.Add(null, Create("/", order: 2), 2);
|
||||
data.Add(null, Create("/", order: null), null);
|
||||
|
||||
// We don't a test case for (left = null, right = null) as it is already tested in another test
|
||||
// and will produce a null ReflectedAttributeRouteModel, which complicates the test case.
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> RightOverridesReflectedAttributeRouteModelTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
|
||||
var leftModel = Create("Home", order: 3);
|
||||
|
||||
data.Add(leftModel, Create("/"));
|
||||
data.Add(leftModel, Create("~/"));
|
||||
data.Add(null, Create("/"));
|
||||
data.Add(null, Create("~/"));
|
||||
data.Add(Create(null), Create("/"));
|
||||
data.Add(Create(null), Create("~/"));
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> NullOrNullTemplateReflectedAttributeRouteModelTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
|
||||
data.Add(null, null);
|
||||
data.Add(null, Create(null));
|
||||
data.Add(Create(null), null);
|
||||
data.Add(Create(null), Create(null));
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ValidReflectedAttributeRouteModelsTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
|
||||
data.Add(null, Create("Index"), Create("Index"));
|
||||
data.Add(Create(null), Create("Index"), Create("Index"));;
|
||||
data.Add(Create("Home"), null, Create("Home"));
|
||||
data.Add(Create("Home"), Create(null), Create("Home"));
|
||||
data.Add(Create("Home"), Create("Index"), Create("Home/Index"));
|
||||
data.Add(Create("Blog"), Create("/Index"), Create("Index"));
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ReplaceTokens_ValueValuesData
|
||||
{
|
||||
get
|
||||
|
|
@ -182,24 +386,6 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ReplaceTokens_ValueValuesData")]
|
||||
public void ReplaceTokens_ValidValues(string template, object values, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var valuesDictionary = values as IDictionary<string, object>;
|
||||
if (valuesDictionary == null)
|
||||
{
|
||||
valuesDictionary = new RouteValueDictionary(values);
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = ReflectedAttributeRouteModel.ReplaceTokens(template, valuesDictionary);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ReplaceTokens_InvalidFormatValuesData
|
||||
{
|
||||
get
|
||||
|
|
@ -265,116 +451,12 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ReplaceTokens_InvalidFormatValuesData")]
|
||||
public void ReplaceTokens_InvalidFormat(string template, object values, string reason)
|
||||
{
|
||||
// Arrange
|
||||
var valuesDictionary = values as IDictionary<string, object>;
|
||||
if (valuesDictionary == null)
|
||||
{
|
||||
valuesDictionary = new RouteValueDictionary(values);
|
||||
}
|
||||
|
||||
var expected = string.Format(
|
||||
"The route template '{0}' has invalid syntax. {1}",
|
||||
template,
|
||||
reason);
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { ReflectedAttributeRouteModel.ReplaceTokens(template, valuesDictionary); });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceTokens_UnknownValue()
|
||||
{
|
||||
// Arrange
|
||||
var template = "[area]/[controller]/[action2]";
|
||||
var values = new RouteValueDictionary()
|
||||
{
|
||||
{ "area", "Help" },
|
||||
{ "controller", "Admin" },
|
||||
{ "action", "SeeUsers" },
|
||||
};
|
||||
|
||||
var expected =
|
||||
"While processing template '[area]/[controller]/[action2]', " +
|
||||
"a replacement value for the token 'action2' could not be found. " +
|
||||
"Available tokens: 'area, controller, action'.";
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => { ReflectedAttributeRouteModel.ReplaceTokens(template, values); });
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("ValidReflectedAttributeRouteModelsTestData")]
|
||||
public void Combine_ValidReflectedAttributeRouteModels(
|
||||
ReflectedAttributeRouteModel left,
|
||||
ReflectedAttributeRouteModel right,
|
||||
ReflectedAttributeRouteModel expectedResult)
|
||||
{
|
||||
// Arrange & Act
|
||||
var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(combined);
|
||||
Assert.Equal(expectedResult.Template, combined.Template);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData("NullOrNullTemplateReflectedAttributeRouteModelTestData")]
|
||||
public void Combine_NullOrNullTemplateReflectedAttributeRouteModels(
|
||||
ReflectedAttributeRouteModel left,
|
||||
ReflectedAttributeRouteModel right)
|
||||
{
|
||||
// Arrange & Act
|
||||
var combined = ReflectedAttributeRouteModel.CombineReflectedAttributeRouteModel(left, right);
|
||||
|
||||
// Assert
|
||||
Assert.Null(combined);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> NullOrNullTemplateReflectedAttributeRouteModelTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
|
||||
data.Add(null, null);
|
||||
data.Add(null, Create(null));
|
||||
data.Add(Create(null), null);
|
||||
data.Add(Create(null), Create(null));
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<object []> ValidReflectedAttributeRouteModelsTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<ReflectedAttributeRouteModel, ReflectedAttributeRouteModel, ReflectedAttributeRouteModel>();
|
||||
data.Add(null, Create("Index"), Create("Index"));
|
||||
data.Add(Create("Home"), null, Create("Home"));
|
||||
data.Add(Create("Home"), Create("Index"), Create("Home/Index"));
|
||||
data.Add(Create("Blog"), Create("/Index"), Create("Index"));
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
private static ReflectedAttributeRouteModel Create(string template)
|
||||
private static ReflectedAttributeRouteModel Create(string template, int? order = null)
|
||||
{
|
||||
return new ReflectedAttributeRouteModel
|
||||
{
|
||||
Template = template
|
||||
Template = template,
|
||||
Order = order
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
using Microsoft.AspNet.Mvc.Routing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
public class RouteTemplateProviderAttributesTest
|
||||
{
|
||||
[Theory]
|
||||
[MemberData("RouteTemplateProvidersTestData")]
|
||||
public void Order_Defaults_ToNull(IRouteTemplateProvider routeTemplateProvider)
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Null(routeTemplateProvider.Order);
|
||||
}
|
||||
|
||||
public static TheoryData<IRouteTemplateProvider> RouteTemplateProvidersTestData
|
||||
{
|
||||
get
|
||||
{
|
||||
var data = new TheoryData<IRouteTemplateProvider>();
|
||||
data.Add(new HttpGetAttribute());
|
||||
data.Add(new HttpPostAttribute());
|
||||
data.Add(new HttpPutAttribute());
|
||||
data.Add(new HttpPatchAttribute());
|
||||
data.Add(new HttpDeleteAttribute());
|
||||
data.Add(new RouteAttribute(""));
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,11 +13,372 @@ using Microsoft.Framework.OptionsModel;
|
|||
using Microsoft.Framework.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using Microsoft.AspNet.PipelineCore;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Routing
|
||||
{
|
||||
public class AttributeRouteTests
|
||||
public class AttributeRouteTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("template/5", "template/{parameter:int}")]
|
||||
[InlineData("template/5", "template/{parameter}")]
|
||||
[InlineData("template/5", "template/{*parameter:int}")]
|
||||
[InlineData("template/5", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter}")]
|
||||
[InlineData("template/{*parameter:int}", "template/{*parameter}")]
|
||||
public async Task AttributeRoute_RouteAsync_RespectsPrecedence(
|
||||
string firstTemplate,
|
||||
string secondTemplate)
|
||||
{
|
||||
// Arrange
|
||||
var expectedRouteGroup = string.Format("{0}&&{1}", 0, firstTemplate);
|
||||
|
||||
// We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
|
||||
var numberOfCalls = 0;
|
||||
Action<RouteContext> callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
|
||||
|
||||
var next = new Mock<IRouter>();
|
||||
next.Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
|
||||
.Callback(callBack)
|
||||
.Returns(Task.FromResult(true))
|
||||
.Verifiable();
|
||||
|
||||
var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 0);
|
||||
var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0);
|
||||
|
||||
// We setup the route entries in reverse order of precedence to ensure that when we
|
||||
// try to route the request, the route with a higher precedence gets tried first.
|
||||
var matchingRoutes = new[] { secondRoute, firstRoute };
|
||||
|
||||
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateRouteContext("/template/5");
|
||||
|
||||
// Act
|
||||
await route.RouteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("template/5", "template/{parameter:int}")]
|
||||
[InlineData("template/5", "template/{parameter}")]
|
||||
[InlineData("template/5", "template/{*parameter:int}")]
|
||||
[InlineData("template/5", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter}")]
|
||||
[InlineData("template/{*parameter:int}", "template/{*parameter}")]
|
||||
public async Task AttributeRoute_RouteAsync_RespectsOrderOverPrecedence(
|
||||
string firstTemplate,
|
||||
string secondTemplate)
|
||||
{
|
||||
// Arrange
|
||||
var expectedRouteGroup = string.Format("{0}&&{1}", 0, secondTemplate);
|
||||
|
||||
// We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
|
||||
var numberOfCalls = 0;
|
||||
Action<RouteContext> callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
|
||||
|
||||
var next = new Mock<IRouter>();
|
||||
next.Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
|
||||
.Callback(callBack)
|
||||
.Returns(Task.FromResult(true))
|
||||
.Verifiable();
|
||||
|
||||
var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 1);
|
||||
var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0);
|
||||
|
||||
// We setup the route entries with a lower relative order and higher relative precedence
|
||||
// first to ensure that when we try to route the request, the route with the higher
|
||||
// relative order gets tried first.
|
||||
var matchingRoutes = new[] { firstRoute, secondRoute };
|
||||
|
||||
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateRouteContext("/template/5");
|
||||
|
||||
// Act
|
||||
await route.RouteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("template/5")]
|
||||
[InlineData("template/{parameter:int}")]
|
||||
[InlineData("template/{parameter}")]
|
||||
[InlineData("template/{*parameter:int}")]
|
||||
[InlineData("template/{*parameter}")]
|
||||
public async Task AttributeRoute_RouteAsync_RespectsOrder(string template)
|
||||
{
|
||||
// Arrange
|
||||
var expectedRouteGroup = string.Format("{0}&&{1}", 0, template);
|
||||
|
||||
// We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
|
||||
var numberOfCalls = 0;
|
||||
Action<RouteContext> callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
|
||||
|
||||
var next = new Mock<IRouter>();
|
||||
next.Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
|
||||
.Callback(callBack)
|
||||
.Returns(Task.FromResult(true))
|
||||
.Verifiable();
|
||||
|
||||
var firstRoute = CreateMatchingEntry(next.Object, template, order: 1);
|
||||
var secondRoute = CreateMatchingEntry(next.Object, template, order: 0);
|
||||
|
||||
// We setup the route entries with a lower relative order first to ensure that when
|
||||
// we try to route the request, the route with the higher relative order gets tried first.
|
||||
var matchingRoutes = new[] { firstRoute, secondRoute };
|
||||
|
||||
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateRouteContext("/template/5");
|
||||
|
||||
// Act
|
||||
await route.RouteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("template/{first:int}", "template/{second:int}")]
|
||||
[InlineData("template/{first}", "template/{second}")]
|
||||
[InlineData("template/{*first:int}", "template/{*second:int}")]
|
||||
[InlineData("template/{*first}", "template/{*second}")]
|
||||
public async Task AttributeRoute_RouteAsync_EnsuresStableOrdering(string first, string second)
|
||||
{
|
||||
// Arrange
|
||||
var expectedRouteGroup = string.Format("{0}&&{1}", 0, first);
|
||||
|
||||
// We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
|
||||
var numberOfCalls = 0;
|
||||
Action<RouteContext> callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
|
||||
|
||||
var next = new Mock<IRouter>();
|
||||
next.Setup(r => r.RouteAsync(It.IsAny<RouteContext>()))
|
||||
.Callback(callBack)
|
||||
.Returns(Task.FromResult(true))
|
||||
.Verifiable();
|
||||
|
||||
var secondRouter = new Mock<IRouter>(MockBehavior.Strict);
|
||||
|
||||
var firstRoute = CreateMatchingEntry(next.Object, first, order: 0);
|
||||
var secondRoute = CreateMatchingEntry(next.Object, second, order: 0);
|
||||
|
||||
// We setup the route entries with a lower relative template order first to ensure that when
|
||||
// we try to route the request, the route with the higher template order gets tried first.
|
||||
var matchingRoutes = new[] { secondRoute, firstRoute };
|
||||
|
||||
var linkGenerationEntries = Enumerable.Empty<AttributeRouteLinkGenerationEntry>();
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateRouteContext("/template/5");
|
||||
|
||||
// Act
|
||||
await route.RouteAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("template/5", "template/{parameter:int}")]
|
||||
[InlineData("template/5", "template/{parameter}")]
|
||||
[InlineData("template/5", "template/{*parameter:int}")]
|
||||
[InlineData("template/5", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter}")]
|
||||
[InlineData("template/{*parameter:int}", "template/{*parameter}")]
|
||||
public void AttributeRoute_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate)
|
||||
{
|
||||
// Arrange
|
||||
var expectedGroup = CreateRouteGroup(0, firstTemplate);
|
||||
|
||||
string selectedGroup = null;
|
||||
|
||||
var next = new Mock<IRouter>();
|
||||
next.Setup(n => n.GetVirtualPath(It.IsAny<VirtualPathContext>())).Callback<VirtualPathContext>(ctx =>
|
||||
{
|
||||
selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
|
||||
ctx.IsBound = true;
|
||||
})
|
||||
.Returns((string)null);
|
||||
|
||||
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>();
|
||||
|
||||
var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null);
|
||||
var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
|
||||
|
||||
// We setup the route entries in reverse order of precedence to ensure that when we
|
||||
// try to generate a link, the route with a higher precedence gets tried first.
|
||||
var linkGenerationEntries = new[] { secondEntry, firstEntry };
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 });
|
||||
|
||||
// Act
|
||||
string result = route.GetVirtualPath(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("template/5", result);
|
||||
Assert.Equal(expectedGroup, selectedGroup);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("template/5", "template/{parameter:int}")]
|
||||
[InlineData("template/5", "template/{parameter}")]
|
||||
[InlineData("template/5", "template/{*parameter:int}")]
|
||||
[InlineData("template/5", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{parameter}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter:int}", "template/{*parameter}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter:int}")]
|
||||
[InlineData("template/{parameter}", "template/{*parameter}")]
|
||||
[InlineData("template/{*parameter:int}", "template/{*parameter}")]
|
||||
public void AttributeRoute_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate)
|
||||
{
|
||||
// Arrange
|
||||
var selectedGroup = CreateRouteGroup(0, secondTemplate);
|
||||
|
||||
string firstRouteGroupSelected = null;
|
||||
var next = new Mock<IRouter>();
|
||||
next.Setup(n => n.GetVirtualPath(It.IsAny<VirtualPathContext>())).Callback<VirtualPathContext>(ctx =>
|
||||
{
|
||||
firstRouteGroupSelected = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
|
||||
ctx.IsBound = true;
|
||||
})
|
||||
.Returns((string)null);
|
||||
|
||||
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>();
|
||||
|
||||
var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1);
|
||||
var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
|
||||
|
||||
// We setup the route entries with a lower relative order and higher relative precedence
|
||||
// first to ensure that when we try to generate a link, the route with the higher
|
||||
// relative order gets tried first.
|
||||
var linkGenerationEntries = new[] { firstRoute, secondRoute };
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 });
|
||||
|
||||
// Act
|
||||
string result = route.GetVirtualPath(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("template/5", result);
|
||||
Assert.Equal(selectedGroup, firstRouteGroupSelected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("template/5", "template/5")]
|
||||
[InlineData("template/{first:int}", "template/{second:int}")]
|
||||
[InlineData("template/{first}", "template/{second}")]
|
||||
[InlineData("template/{*first:int}", "template/{*second:int}")]
|
||||
[InlineData("template/{*first}", "template/{*second}")]
|
||||
public void AttributeRoute_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate)
|
||||
{
|
||||
// Arrange
|
||||
var expectedGroup = CreateRouteGroup(0, secondTemplate);
|
||||
|
||||
var next = new Mock<IRouter>();
|
||||
string selectedGroup = null;
|
||||
next.Setup(n => n.GetVirtualPath(It.IsAny<VirtualPathContext>())).Callback<VirtualPathContext>(ctx =>
|
||||
{
|
||||
selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
|
||||
ctx.IsBound = true;
|
||||
})
|
||||
.Returns((string)null);
|
||||
|
||||
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>();
|
||||
|
||||
var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1);
|
||||
var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
|
||||
|
||||
// We setup the route entries with a lower relative order first to ensure that when
|
||||
// we try to generate a link, the route with the higher relative order gets tried first.
|
||||
var linkGenerationEntries = new[] { firstRoute, secondRoute };
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 });
|
||||
|
||||
// Act
|
||||
string result = route.GetVirtualPath(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("template/5", result);
|
||||
Assert.Equal(expectedGroup, selectedGroup);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("first/5", "second/5")]
|
||||
[InlineData("first/{first:int}", "second/{second:int}")]
|
||||
[InlineData("first/{first}", "second/{second}")]
|
||||
[InlineData("first/{*first:int}", "second/{*second:int}")]
|
||||
[InlineData("first/{*first}", "second/{*second}")]
|
||||
public void AttributeRoute_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate)
|
||||
{
|
||||
// Arrange
|
||||
var expectedGroup = CreateRouteGroup(0, firstTemplate);
|
||||
|
||||
var next = new Mock<IRouter>();
|
||||
string selectedGroup = null;
|
||||
next.Setup(n => n.GetVirtualPath(It.IsAny<VirtualPathContext>())).Callback<VirtualPathContext>(ctx =>
|
||||
{
|
||||
selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
|
||||
ctx.IsBound = true;
|
||||
})
|
||||
.Returns((string)null);
|
||||
|
||||
var matchingRoutes = Enumerable.Empty<AttributeRouteMatchingEntry>();
|
||||
|
||||
var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 0);
|
||||
var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
|
||||
|
||||
// We setup the route entries with a lower relative template order first to ensure that when
|
||||
// we try to generate a link, the route with the higher template order gets tried first.
|
||||
var linkGenerationEntries = new[] { secondRoute, firstRoute };
|
||||
|
||||
var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
|
||||
|
||||
var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 });
|
||||
|
||||
// Act
|
||||
string result = route.GetVirtualPath(context);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("first/5", result);
|
||||
Assert.Equal(expectedGroup, selectedGroup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void AttributeRoute_RouteAsyncHandled_LogsCorrectValues()
|
||||
{
|
||||
|
|
@ -55,7 +416,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
Assert.Equal(true, values.Handled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact]
|
||||
public async void AttributeRoute_RouteAsyncNotHandled_LogsCorrectValues()
|
||||
{
|
||||
// Arrange
|
||||
|
|
@ -291,7 +652,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" });
|
||||
var route = CreateAttributeRoute(entry);
|
||||
|
||||
var context = CreateVirtualPathContext(new { action = "Index", id = 5}, new { controller = "Store" });
|
||||
var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" });
|
||||
|
||||
// Act
|
||||
var path = route.GetVirtualPath(context);
|
||||
|
|
@ -356,7 +717,7 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
// Reject entry 1.
|
||||
callCount++;
|
||||
return !c.ProvidedValues.Contains(new KeyValuePair<string, object>(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
AttributeRouting.RouteGroupKey,
|
||||
entry1.RouteGroup));
|
||||
};
|
||||
|
||||
|
|
@ -489,16 +850,34 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
.Returns(NullLoggerFactory.Instance);
|
||||
|
||||
return new VirtualPathContext(
|
||||
mockHttpContext.Object,
|
||||
new RouteValueDictionary(ambientValues),
|
||||
mockHttpContext.Object,
|
||||
new RouteValueDictionary(ambientValues),
|
||||
new RouteValueDictionary(values));
|
||||
}
|
||||
|
||||
private static AttributeRouteLinkGenerationEntry CreateGenerationEntry(string template, object requiredValues)
|
||||
private static AttributeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order)
|
||||
{
|
||||
var constraintResolver = CreateConstraintResolver();
|
||||
|
||||
var routeTemplate = TemplateParser.Parse(template, constraintResolver);
|
||||
|
||||
var entry = new AttributeRouteMatchingEntry();
|
||||
entry.Route = new TemplateRoute(router, template, constraintResolver);
|
||||
entry.Precedence = AttributeRoutePrecedence.Compute(routeTemplate);
|
||||
entry.Order = order;
|
||||
|
||||
string routeGroup = string.Format("{0}&&{1}", order, template);
|
||||
entry.Route.Defaults.Add("test_route_group", routeGroup);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private static AttributeRouteLinkGenerationEntry CreateGenerationEntry(string template, object requiredValues, int order = 0)
|
||||
{
|
||||
var constraintResolver = CreateConstraintResolver();
|
||||
|
||||
var entry = new AttributeRouteLinkGenerationEntry();
|
||||
entry.TemplateText = template;
|
||||
entry.Template = TemplateParser.Parse(template, constraintResolver);
|
||||
|
||||
var defaults = entry.Template.Parameters
|
||||
|
|
@ -512,9 +891,10 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
entry.Constraints = constraints;
|
||||
entry.Defaults = defaults;
|
||||
entry.Binder = new TemplateBinder(entry.Template, defaults);
|
||||
entry.Order = order;
|
||||
entry.Precedence = AttributeRoutePrecedence.Compute(entry.Template);
|
||||
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
|
||||
entry.RouteGroup = template;
|
||||
entry.RouteGroup = CreateRouteGroup(order, template);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
|
@ -543,6 +923,11 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
return entry;
|
||||
}
|
||||
|
||||
private static string CreateRouteGroup(int order, string template)
|
||||
{
|
||||
return string.Format("{0}&{1}", order, template);
|
||||
}
|
||||
|
||||
private static DefaultInlineConstraintResolver CreateConstraintResolver()
|
||||
{
|
||||
var services = Mock.Of<IServiceProvider>();
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
{
|
||||
{ "controller", "Home" },
|
||||
{ "action", "Index" },
|
||||
},
|
||||
},
|
||||
result.RouteValues);
|
||||
}
|
||||
|
||||
|
|
@ -360,7 +360,83 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
result.RouteValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_OverrideActionOverridesOrderOnController()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.Handler;
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Team/5");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Contains("/Team/5", result.ExpectedUrls);
|
||||
Assert.Equal("Team", result.Controller);
|
||||
Assert.Equal("GetOrganization", result.Action);
|
||||
|
||||
Assert.Contains(
|
||||
new KeyValuePair<string, object>("teamId", "5"),
|
||||
result.RouteValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_OrderOnActionOverridesOrderOnController()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.Handler;
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("http://localhost/Teams");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, response.StatusCode);
|
||||
|
||||
var body = await response.ReadBodyAsStringAsync();
|
||||
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||
|
||||
Assert.Contains("/Teams", result.ExpectedUrls);
|
||||
Assert.Equal("Team", result.Controller);
|
||||
Assert.Equal("GetOrganizations", result.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_LinkGeneration_OverrideActionOverridesOrderOnController()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.Handler;
|
||||
|
||||
// Act
|
||||
var response = await client.GetStringAsync("http://localhost/Organization/5");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal("/Club/5", response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction__LinkGeneration_OrderOnActionOverridesOrderOnController()
|
||||
{
|
||||
// Arrange
|
||||
var server = TestServer.Create(_services, _app);
|
||||
var client = server.Handler;
|
||||
|
||||
// Act
|
||||
var response = await client.GetStringAsync("http://localhost/Teams/AllTeams");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal("/Teams/AllOrganizations", response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AttributeRoutedAction_LinkToSelf()
|
||||
{
|
||||
// Arrange
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
using Microsoft.AspNet.Mvc;
|
||||
using System;
|
||||
|
||||
namespace RoutingWebSite
|
||||
{
|
||||
[Route("/Teams", Order = 1)]
|
||||
public class TeamController : Controller
|
||||
{
|
||||
private readonly TestResponseGenerator _generator;
|
||||
|
||||
public TeamController(TestResponseGenerator generator)
|
||||
{
|
||||
_generator = generator;
|
||||
}
|
||||
|
||||
[HttpGet("/Team/{teamId}", Order = 2)]
|
||||
public ActionResult GetTeam(int teamId)
|
||||
{
|
||||
return _generator.Generate("/Team/" + teamId);
|
||||
}
|
||||
|
||||
[HttpGet("/Team/{teamId}")]
|
||||
public ActionResult GetOrganization(int teamId)
|
||||
{
|
||||
return _generator.Generate("/Team/" + teamId);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public ActionResult GetTeams()
|
||||
{
|
||||
return _generator.Generate("/Teams");
|
||||
}
|
||||
|
||||
[HttpGet("", Order = 0)]
|
||||
public ActionResult GetOrganizations()
|
||||
{
|
||||
return _generator.Generate("/Teams");
|
||||
}
|
||||
|
||||
[HttpGet("/Club/{clubId?}")]
|
||||
public ActionResult GetClub()
|
||||
{
|
||||
return Content(Url.Action(),"text/plain");
|
||||
}
|
||||
|
||||
[HttpGet("/Organization/{clubId?}", Order = 1)]
|
||||
public ActionResult GetClub(int clubId)
|
||||
{
|
||||
return Content(Url.Action(), "text/plain");
|
||||
}
|
||||
|
||||
[HttpGet("AllTeams")]
|
||||
public ActionResult GetAllTeams()
|
||||
{
|
||||
return Content(Url.Action(), "text/plain");
|
||||
}
|
||||
|
||||
[HttpGet("AllOrganizations", Order = 0)]
|
||||
public ActionResult GetAllTeams(int notRelevant)
|
||||
{
|
||||
return Content(Url.Action(), "text/plain");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,10 @@ namespace RoutingWebSite
|
|||
get { return _supportedMethods; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Template { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? Order { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,7 @@
|
|||
<Compile Include="Controllers\EmployeeController.cs" />
|
||||
<Compile Include="Controllers\HomeController.cs" />
|
||||
<Compile Include="Controllers\StoreController.cs" />
|
||||
<Compile Include="Controllers\TeamController.cs" />
|
||||
<Compile Include="HttpMergeAttribute.cs" />
|
||||
<Compile Include="Startup.cs" />
|
||||
<Compile Include="TestResponseGenerator.cs" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue