Adding Attribute Routing Link Generation
This commit is contained in:
parent
340bd7550a
commit
745239f09f
|
|
@ -16,5 +16,11 @@ namespace MvcSample.Web
|
||||||
{
|
{
|
||||||
return "Get other thing";
|
return "Get other thing";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("Link")]
|
||||||
|
public string GenerateLink(string action = null, string controller = null)
|
||||||
|
{
|
||||||
|
return Url.Action(action, controller);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNet.Mvc.Routing;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc
|
namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
|
|
@ -13,7 +12,12 @@ namespace Microsoft.AspNet.Mvc
|
||||||
public List<RouteDataActionConstraint> RouteConstraints { get; set; }
|
public List<RouteDataActionConstraint> RouteConstraints { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The route template May be null if the action has no attribute routes.
|
/// The set of route values that are added when this action is selected.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object> RouteValues { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The route template. May be null if the action has no attribute routes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string RouteTemplate { get; set; }
|
public string RouteTemplate { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,8 @@
|
||||||
<Compile Include="KnownRouteValueConstraint.cs" />
|
<Compile Include="KnownRouteValueConstraint.cs" />
|
||||||
<Compile Include="RouteKeyHandling.cs" />
|
<Compile Include="RouteKeyHandling.cs" />
|
||||||
<Compile Include="Routing\AttributeRoute.cs" />
|
<Compile Include="Routing\AttributeRoute.cs" />
|
||||||
<Compile Include="Routing\AttributeRouteEntry.cs" />
|
<Compile Include="Routing\AttributeRouteGenerationEntry.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRouteMatchingEntry.cs" />
|
||||||
<Compile Include="Routing\AttributeRoutePrecedence.cs" />
|
<Compile Include="Routing\AttributeRoutePrecedence.cs" />
|
||||||
<Compile Include="Routing\AttributeRouteTemplate.cs" />
|
<Compile Include="Routing\AttributeRouteTemplate.cs" />
|
||||||
<Compile Include="Routing\AttributeRouting.cs" />
|
<Compile Include="Routing\AttributeRouting.cs" />
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,17 @@ namespace Microsoft.AspNet.Mvc
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actionDescriptor.RouteValues != null)
|
||||||
|
{
|
||||||
|
foreach (var kvp in actionDescriptor.RouteValues)
|
||||||
|
{
|
||||||
|
if (!context.RouteData.Values.ContainsKey(kvp.Key))
|
||||||
|
{
|
||||||
|
context.RouteData.Values.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var actionContext = new ActionContext(context.HttpContext, context.RouteData, actionDescriptor);
|
var actionContext = new ActionContext(context.HttpContext, context.RouteData, actionDescriptor);
|
||||||
|
|
||||||
var contextAccessor = services.GetService<IContextAccessor<ActionContext>>();
|
var contextAccessor = services.GetService<IContextAccessor<ActionContext>>();
|
||||||
|
|
|
||||||
|
|
@ -211,26 +211,41 @@ namespace Microsoft.AspNet.Mvc
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// An attribute routed action will ignore conventional routed constraints.
|
// An attribute routed action will ignore conventional routed constraints. We still
|
||||||
actionDescriptor.RouteConstraints.Clear();
|
// want to provide these values as ambient values.
|
||||||
|
var ambientValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
// TODO #738 - this currently has parity with what we did in MVC5 for the action
|
foreach (var constraint in actionDescriptor.RouteConstraints)
|
||||||
// route values. This needs to be reconsidered as part of #738.
|
|
||||||
var template = TemplateParser.Parse(templateText, _constraintResolver);
|
|
||||||
if (template.Parameters.Any(
|
|
||||||
p => p.IsParameter &&
|
|
||||||
string.Equals(p.Name, "action", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
ambientValues.Add(constraint.RouteKey, constraint.RouteValue);
|
||||||
"action",
|
}
|
||||||
action.ActionName));
|
|
||||||
|
actionDescriptor.RouteValues = ambientValues;
|
||||||
|
|
||||||
|
// TODO #738 - this currently has parity with what we did in MVC5 when a template uses parameters
|
||||||
|
// like 'area', 'controller', and 'action. This needs to be reconsidered as part of #738.
|
||||||
|
//
|
||||||
|
// For instance, consider actions mapped with api/Blog/{action}. The value of {action} needs to
|
||||||
|
// passed to action selection to choose the right action.
|
||||||
|
var template = TemplateParser.Parse(templateText, _constraintResolver);
|
||||||
|
|
||||||
|
var routeConstraints = new List<RouteDataActionConstraint>();
|
||||||
|
foreach (var constraint in actionDescriptor.RouteConstraints)
|
||||||
|
{
|
||||||
|
if (template.Parameters.Any(
|
||||||
|
p => p.IsParameter &&
|
||||||
|
string.Equals(p.Name, constraint.RouteKey, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
routeConstraints.Add(constraint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var routeGroup = routeGroupsByTemplate[templateText];
|
var routeGroup = routeGroupsByTemplate[templateText];
|
||||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
routeConstraints.Add(new RouteDataActionConstraint(
|
||||||
AttributeRouting.RouteGroupKey,
|
AttributeRouting.RouteGroupKey,
|
||||||
routeGroup));
|
routeGroup));
|
||||||
|
|
||||||
|
actionDescriptor.RouteConstraints = routeConstraints;
|
||||||
|
|
||||||
actionDescriptor.RouteTemplate = templateText;
|
actionDescriptor.RouteTemplate = templateText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,11 +265,21 @@ namespace Microsoft.AspNet.Mvc
|
||||||
{
|
{
|
||||||
foreach (var key in removalConstraints)
|
foreach (var key in removalConstraints)
|
||||||
{
|
{
|
||||||
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
|
if (actionDescriptor.RouteTemplate == null)
|
||||||
{
|
{
|
||||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
|
||||||
key,
|
{
|
||||||
RouteKeyHandling.DenyKey));
|
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||||
|
key,
|
||||||
|
RouteKeyHandling.DenyKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!actionDescriptor.RouteValues.ContainsKey(key))
|
||||||
|
{
|
||||||
|
actionDescriptor.RouteValues.Add(key, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -282,4 +307,4 @@ namespace Microsoft.AspNet.Mvc
|
||||||
return groupsByTemplate;
|
return groupsByTemplate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
// 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.
|
// 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 System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -15,26 +16,34 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
public class AttributeRoute : IRouter
|
public class AttributeRoute : IRouter
|
||||||
{
|
{
|
||||||
private readonly IRouter _next;
|
private readonly IRouter _next;
|
||||||
private readonly TemplateRoute[] _routes;
|
private readonly TemplateRoute[] _matchingRoutes;
|
||||||
|
private readonly AttributeRouteGenerationEntry[] _generationEntries;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="AttributeRoute"/>.
|
/// Creates a new <see cref="AttributeRoute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next">The next router. Invoked when a route entry matches.</param>
|
/// <param name="next">The next router. Invoked when a route entry matches.</param>
|
||||||
/// <param name="entries">The set of route entries.</param>
|
/// <param name="entries">The set of route entries.</param>
|
||||||
public AttributeRoute([NotNull] IRouter next, [NotNull] IEnumerable<AttributeRouteEntry> entries)
|
public AttributeRoute(
|
||||||
|
[NotNull] IRouter next,
|
||||||
|
[NotNull] IEnumerable<AttributeRouteMatchingEntry> matchingEntries,
|
||||||
|
[NotNull] IEnumerable<AttributeRouteGenerationEntry> generationEntries)
|
||||||
{
|
{
|
||||||
_next = next;
|
_next = next;
|
||||||
|
|
||||||
// FOR RIGHT NOW - this is just an array of regular template routes. We'll follow up by implementing
|
// FOR RIGHT NOW - this is just an array of regular template routes. We'll follow up by implementing
|
||||||
// a good data-structure here.
|
// a good data-structure here. See #740
|
||||||
_routes = entries.OrderBy(e => e.Precedence).Select(e => e.Route).ToArray();
|
_matchingRoutes = matchingEntries.OrderBy(e => e.Precedence).Select(e => e.Route).ToArray();
|
||||||
|
|
||||||
|
// FOR RIGHT NOW - this is just an array of binders. We'll follow up by implementing
|
||||||
|
// a good data-structure here. See #741
|
||||||
|
_generationEntries = generationEntries.OrderBy(e => e.Precedence).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task RouteAsync([NotNull] RouteContext context)
|
public async Task RouteAsync([NotNull] RouteContext context)
|
||||||
{
|
{
|
||||||
foreach (var route in _routes)
|
foreach (var route in _matchingRoutes)
|
||||||
{
|
{
|
||||||
await route.RouteAsync(context);
|
await route.RouteAsync(context);
|
||||||
if (context.IsHandled)
|
if (context.IsHandled)
|
||||||
|
|
@ -47,9 +56,127 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetVirtualPath([NotNull] VirtualPathContext context)
|
public string GetVirtualPath([NotNull] VirtualPathContext context)
|
||||||
{
|
{
|
||||||
// Not implemented right now, but we don't want to throw here and block other routes from generating
|
// To generate a link, we iterate the collection of entries (in order of precedence) and execute
|
||||||
// a link.
|
// each one that matches the 'required link values' - which will typically be a value for action
|
||||||
|
// and controller.
|
||||||
|
//
|
||||||
|
// Building a proper data structure to optimize this is tracked by #741
|
||||||
|
foreach (var entry in _generationEntries)
|
||||||
|
{
|
||||||
|
var isMatch = true;
|
||||||
|
foreach (var requiredLinkValue in entry.RequiredLinkValues)
|
||||||
|
{
|
||||||
|
if (!ContextHasSameValue(context, requiredLinkValue.Key, requiredLinkValue.Value))
|
||||||
|
{
|
||||||
|
isMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMatch)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = GenerateLink(context, entry);
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
context.IsBound = true;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GenerateLink(VirtualPathContext context, AttributeRouteGenerationEntry entry)
|
||||||
|
{
|
||||||
|
// In attribute the context includes the values that are used to select this entry - typically
|
||||||
|
// these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't
|
||||||
|
// want to pass these to the link generation code, or else they will end up as query parameters.
|
||||||
|
//
|
||||||
|
// So, we need to exclude from here any values that are 'required link values', but aren't
|
||||||
|
// parameters in the template.
|
||||||
|
//
|
||||||
|
// Ex:
|
||||||
|
// template: api/Products/{action}
|
||||||
|
// required values: { id = "5", action = "Buy", Controller = "CoolProducts" }
|
||||||
|
//
|
||||||
|
// result: { id = "5", action = "Buy" }
|
||||||
|
var inputValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var kvp in context.Values)
|
||||||
|
{
|
||||||
|
if (entry.RequiredLinkValues.ContainsKey(kvp.Key))
|
||||||
|
{
|
||||||
|
var parameter = entry.Template.Parameters
|
||||||
|
.FirstOrDefault(p => string.Equals(p.Name, kvp.Key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (parameter == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputValues.Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindingResult = entry.Binder.GetValues(context.AmbientValues, inputValues);
|
||||||
|
if (bindingResult == null)
|
||||||
|
{
|
||||||
|
// A required parameter in the template didn't get a value.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matched = RouteConstraintMatcher.Match(
|
||||||
|
entry.Constraints,
|
||||||
|
bindingResult.CombinedValues,
|
||||||
|
context.Context,
|
||||||
|
this,
|
||||||
|
RouteDirection.UrlGeneration);
|
||||||
|
if (!matched)
|
||||||
|
{
|
||||||
|
// A constrant rejected this link.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// These values are used to signal to the next route what we would produce if we round-tripped
|
||||||
|
// (generate a link and then parse). In MVC the 'next route' is typically the MvcRouteHandler.
|
||||||
|
var providedValues = new Dictionary<string, object>(
|
||||||
|
bindingResult.AcceptedValues,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
providedValues.Add(AttributeRouting.RouteGroupKey, entry.RouteGroup);
|
||||||
|
|
||||||
|
var childContext = new VirtualPathContext(context.Context, context.AmbientValues, context.Values)
|
||||||
|
{
|
||||||
|
ProvidedValues = providedValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
var path = _next.GetVirtualPath(childContext);
|
||||||
|
if (path != null)
|
||||||
|
{
|
||||||
|
// If path is non-null then the target router short-circuited, we don't expect this
|
||||||
|
// in typical MVC scenarios.
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
else if (!childContext.IsBound)
|
||||||
|
{
|
||||||
|
// The target router has rejected these values. We don't expect this in typical MVC scenarios.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = entry.Binder.BindValues(bindingResult.AcceptedValues);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ContextHasSameValue(VirtualPathContext context, string key, object value)
|
||||||
|
{
|
||||||
|
object providedValue;
|
||||||
|
if (!context.Values.TryGetValue(key, out providedValue))
|
||||||
|
{
|
||||||
|
context.AmbientValues.TryGetValue(key, out providedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TemplateBinder.RoutePartsEqual(providedValue, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual URL-generating route that will be
|
||||||
|
/// aggregated into the <see cref="AttributeRoute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class AttributeRouteGenerationEntry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="TemplateBinder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public TemplateBinder Binder { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The route constraints.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, IRouteConstraint> Constraints { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The route defaults.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, object> Defaults { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The precedence of the template.
|
||||||
|
/// </summary>
|
||||||
|
public decimal Precedence { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The route group.
|
||||||
|
/// </summary>
|
||||||
|
public string RouteGroup { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The set of values that must be present for link genration.
|
||||||
|
/// </summary>
|
||||||
|
public IDictionary<string, object> RequiredLinkValues { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="Template"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Template Template { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNet.Routing.Template;
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Routing
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual route that will be aggregated
|
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual URL-matching route that will be
|
||||||
/// into the <see cref="AttributeRoute"/>.
|
/// aggregated into the <see cref="AttributeRoute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AttributeRouteEntry
|
public class AttributeRouteMatchingEntry
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The precedence of the template.
|
/// The precedence of the template.
|
||||||
|
|
@ -21,4 +22,4 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TemplateRoute Route { get; set; }
|
public TemplateRoute Route { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,38 +26,57 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
{
|
{
|
||||||
var actions = GetActionDescriptors(services);
|
var actions = GetActionDescriptors(services);
|
||||||
|
|
||||||
// We're creating one AttributeRouteEntry per group, so we need to identify the distinct set of
|
|
||||||
// groups. It's guaranteed that all members of the group have the same template and precedence,
|
|
||||||
// so we only need to hang on to a single instance of the template.
|
|
||||||
var routeTemplatesByGroup = GroupTemplatesByGroupId(actions);
|
|
||||||
|
|
||||||
var inlineConstraintResolver = services.GetService<IInlineConstraintResolver>();
|
var inlineConstraintResolver = services.GetService<IInlineConstraintResolver>();
|
||||||
|
var routeInfos = GetRouteInfos(actions, inlineConstraintResolver);
|
||||||
|
|
||||||
var entries = new List<AttributeRouteEntry>();
|
// We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended
|
||||||
foreach (var routeGroup in routeTemplatesByGroup)
|
// action by expected route values, and then use the TemplateBinder to generate the link.
|
||||||
|
var generationEntries = new List<AttributeRouteGenerationEntry>();
|
||||||
|
foreach (var routeInfo in routeInfos)
|
||||||
{
|
{
|
||||||
var routeGroupId = routeGroup.Key;
|
var defaults = routeInfo.ParsedTemplate.Parameters
|
||||||
var template = routeGroup.Value;
|
.Where(p => p.DefaultValue != null)
|
||||||
|
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var parsedTemplate = TemplateParser.Parse(template, inlineConstraintResolver);
|
var constraints = routeInfo.ParsedTemplate.Parameters
|
||||||
var precedence = AttributeRoutePrecedence.Compute(parsedTemplate);
|
.Where(p => p.InlineConstraint != null)
|
||||||
|
.ToDictionary(p => p.Name, p => p.InlineConstraint, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
entries.Add(new AttributeRouteEntry()
|
generationEntries.Add(new AttributeRouteGenerationEntry()
|
||||||
{
|
{
|
||||||
Precedence = precedence,
|
Binder = new TemplateBinder(routeInfo.ParsedTemplate, defaults),
|
||||||
|
Defaults = defaults,
|
||||||
|
Constraints = constraints,
|
||||||
|
Precedence = routeInfo.Precedence,
|
||||||
|
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValues,
|
||||||
|
RouteGroup = routeInfo.RouteGroup,
|
||||||
|
Template = routeInfo.ParsedTemplate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're creating one AttributeRouteMatchingEntry per group, so we need to identify the distinct set of
|
||||||
|
// groups. It's guaranteed that all members of the group have the same template and precedence,
|
||||||
|
// so we only need to hang on to a single instance of the RouteInfo for each group.
|
||||||
|
var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos);
|
||||||
|
var matchingEntries = new List<AttributeRouteMatchingEntry>();
|
||||||
|
foreach (var routeInfo in distinctRouteInfosByGroup)
|
||||||
|
{
|
||||||
|
matchingEntries.Add(new AttributeRouteMatchingEntry()
|
||||||
|
{
|
||||||
|
Precedence = routeInfo.Precedence,
|
||||||
Route = new TemplateRoute(
|
Route = new TemplateRoute(
|
||||||
target,
|
target,
|
||||||
template,
|
routeInfo.RouteTemplate,
|
||||||
defaults: new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
defaults: new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ RouteGroupKey, routeGroupId },
|
{ RouteGroupKey, routeInfo.RouteGroup },
|
||||||
},
|
},
|
||||||
constraints: null,
|
constraints: null,
|
||||||
inlineConstraintResolver: inlineConstraintResolver),
|
inlineConstraintResolver: inlineConstraintResolver),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AttributeRoute(target, entries);
|
return new AttributeRoute(target, matchingEntries, generationEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<ActionDescriptor> GetActionDescriptors(IServiceProvider services)
|
private static IReadOnlyList<ActionDescriptor> GetActionDescriptors(IServiceProvider services)
|
||||||
|
|
@ -68,9 +87,27 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
return actionDescriptorsCollection.Items;
|
return actionDescriptorsCollection.Items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<string, string> GroupTemplatesByGroupId(IReadOnlyList<ActionDescriptor> actions)
|
private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos)
|
||||||
{
|
{
|
||||||
var routeTemplatesByGroup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var routeInfosByGroupId = new Dictionary<string, RouteInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var routeInfo in routeInfos)
|
||||||
|
{
|
||||||
|
if (!routeInfosByGroupId.ContainsKey(routeInfo.RouteGroup))
|
||||||
|
{
|
||||||
|
routeInfosByGroupId.Add(routeInfo.RouteGroup, routeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeInfosByGroupId.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<RouteInfo> GetRouteInfos(
|
||||||
|
IReadOnlyList<ActionDescriptor> actions,
|
||||||
|
IInlineConstraintResolver constraintResolver)
|
||||||
|
{
|
||||||
|
var routeInfos = new List<RouteInfo>();
|
||||||
|
|
||||||
foreach (var action in actions.Where(a => a.RouteTemplate != null))
|
foreach (var action in actions.Where(a => a.RouteTemplate != null))
|
||||||
{
|
{
|
||||||
var constraint = action.RouteConstraints
|
var constraint = action.RouteConstraints
|
||||||
|
|
@ -84,14 +121,31 @@ namespace Microsoft.AspNet.Mvc.Routing
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var routeGroup = constraint.RouteValue;
|
var parsedTemplate = TemplateParser.Parse(action.RouteTemplate, constraintResolver);
|
||||||
if (!routeTemplatesByGroup.ContainsKey(routeGroup))
|
routeInfos.Add(new RouteInfo()
|
||||||
{
|
{
|
||||||
routeTemplatesByGroup.Add(routeGroup, action.RouteTemplate);
|
ActionDescriptor = action,
|
||||||
}
|
ParsedTemplate = parsedTemplate,
|
||||||
|
Precedence = AttributeRoutePrecedence.Compute(parsedTemplate),
|
||||||
|
RouteGroup = constraint.RouteValue,
|
||||||
|
RouteTemplate = action.RouteTemplate,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return routeTemplatesByGroup;
|
return routeInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RouteInfo
|
||||||
|
{
|
||||||
|
public ActionDescriptor ActionDescriptor { get; set; }
|
||||||
|
|
||||||
|
public Template ParsedTemplate { get; set; }
|
||||||
|
|
||||||
|
public decimal Precedence { get; set; }
|
||||||
|
|
||||||
|
public string RouteGroup { get; set; }
|
||||||
|
|
||||||
|
public string RouteTemplate { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@
|
||||||
<Compile Include="Routing\AttributeRoutePrecedenceTests.cs" />
|
<Compile Include="Routing\AttributeRoutePrecedenceTests.cs" />
|
||||||
<Compile Include="Routing\AttributeRouteTemplateTests.cs" />
|
<Compile Include="Routing\AttributeRouteTemplateTests.cs" />
|
||||||
<Compile Include="StaticControllerAssemblyProvider.cs" />
|
<Compile Include="StaticControllerAssemblyProvider.cs" />
|
||||||
|
<Compile Include="Routing\AttributeRouteTests.cs" />
|
||||||
<Compile Include="TestController.cs" />
|
<Compile Include="TestController.cs" />
|
||||||
<Compile Include="TypeHelperTest.cs" />
|
<Compile Include="TypeHelperTest.cs" />
|
||||||
<Compile Include="StaticActionDiscoveryConventions.cs" />
|
<Compile Include="StaticActionDiscoveryConventions.cs" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,409 @@
|
||||||
|
// 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 System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.Http;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
|
using Microsoft.Framework.OptionsModel;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Routing
|
||||||
|
{
|
||||||
|
public class AttributeRouteTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_NoRequiredValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store", new { });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_NoMatch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store", new { action = "Details", controller = "Store" });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match_WithAmbientValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match_WithParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store/{action}", new { action = "Index", controller = "Store" });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store/Index", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match_WithMoreParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry(
|
||||||
|
"api/{area}/dosomething/{controller}/{action}",
|
||||||
|
new { action = "Index", controller = "Store", area = "AwesomeCo" });
|
||||||
|
|
||||||
|
var expectedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "area", "AwesomeCo" },
|
||||||
|
{ "controller", "Store" },
|
||||||
|
{ "action", "Index" },
|
||||||
|
{ AttributeRouting.RouteGroupKey, entry.RouteGroup },
|
||||||
|
};
|
||||||
|
|
||||||
|
var next = new StubRouter();
|
||||||
|
var route = CreateAttributeRoute(next, entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(
|
||||||
|
new { action = "Index", controller = "Store" },
|
||||||
|
new { area = "AwesomeCo" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/AwesomeCo/dosomething/Store/Index", path);
|
||||||
|
Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match_WithDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store/{action=Index}", new { action = "Index", controller = "Store" });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match_WithConstraint()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
var expectedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "action", "Index" },
|
||||||
|
{ "id", 5 },
|
||||||
|
{ AttributeRouting.RouteGroupKey, entry.RouteGroup },
|
||||||
|
};
|
||||||
|
|
||||||
|
var next = new StubRouter();
|
||||||
|
var route = CreateAttributeRoute(next, entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store/Index/5", path);
|
||||||
|
Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_NoMatch_WithConstraint()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var expectedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ "id", "5" },
|
||||||
|
{ AttributeRouting.RouteGroupKey, entry.RouteGroup },
|
||||||
|
};
|
||||||
|
|
||||||
|
var next = new StubRouter();
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match_WithMixedAmbientValues()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" });
|
||||||
|
var route = CreateAttributeRoute(entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_Match_WithQueryString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
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" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api/Store?id=5", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_ForwardsRouteGroup()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
var expectedValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
{ AttributeRouting.RouteGroupKey, entry.RouteGroup },
|
||||||
|
};
|
||||||
|
|
||||||
|
var next = new StubRouter();
|
||||||
|
var route = CreateAttributeRoute(next, entry);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_RejectedByFirstRoute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry1 = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" });
|
||||||
|
var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Index", controller = "Blog" });
|
||||||
|
|
||||||
|
var route = CreateAttributeRoute(entry1, entry2);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api2/Blog", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AttributeRoute_GenerateLink_RejectedByHandler()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var entry1 = CreateGenerationEntry("api/Store", new { action = "Edit", controller = "Store" });
|
||||||
|
var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Edit", controller = "Store" });
|
||||||
|
|
||||||
|
var next = new StubRouter();
|
||||||
|
|
||||||
|
var callCount = 0;
|
||||||
|
next.GenerationDelegate = (VirtualPathContext c) =>
|
||||||
|
{
|
||||||
|
// Reject entry 1.
|
||||||
|
callCount++;
|
||||||
|
return !c.ProvidedValues.Contains(new KeyValuePair<string, object>(
|
||||||
|
AttributeRouting.RouteGroupKey,
|
||||||
|
entry1.RouteGroup));
|
||||||
|
};
|
||||||
|
|
||||||
|
var route = CreateAttributeRoute(next, entry1, entry2);
|
||||||
|
|
||||||
|
var context = CreateVirtualPathContext(new { action = "Edit", controller = "Store" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var path = route.GetVirtualPath(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("api2/Store", path);
|
||||||
|
Assert.Equal(2, callCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VirtualPathContext CreateVirtualPathContext(object values, object ambientValues = null)
|
||||||
|
{
|
||||||
|
var httpContext = Mock.Of<HttpContext>();
|
||||||
|
|
||||||
|
return new VirtualPathContext(
|
||||||
|
httpContext,
|
||||||
|
new RouteValueDictionary(ambientValues),
|
||||||
|
new RouteValueDictionary(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttributeRouteGenerationEntry CreateGenerationEntry(string template, object requiredValues)
|
||||||
|
{
|
||||||
|
var constraintResolver = CreateConstraintResolver();
|
||||||
|
|
||||||
|
var entry = new AttributeRouteGenerationEntry();
|
||||||
|
entry.Template = TemplateParser.Parse(template, constraintResolver);
|
||||||
|
|
||||||
|
var defaults = entry.Template.Parameters
|
||||||
|
.Where(p => p.DefaultValue != null)
|
||||||
|
.ToDictionary(p => p.Name, p => p.DefaultValue);
|
||||||
|
|
||||||
|
var constraints = entry.Template.Parameters
|
||||||
|
.Where(p => p.InlineConstraint != null)
|
||||||
|
.ToDictionary(p => p.Name, p => p.InlineConstraint);
|
||||||
|
|
||||||
|
entry.Constraints = constraints;
|
||||||
|
entry.Defaults = defaults;
|
||||||
|
entry.Binder = new TemplateBinder(entry.Template, defaults);
|
||||||
|
entry.Precedence = AttributeRoutePrecedence.Compute(entry.Template);
|
||||||
|
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);
|
||||||
|
entry.RouteGroup = template;
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefaultInlineConstraintResolver CreateConstraintResolver()
|
||||||
|
{
|
||||||
|
var services = Mock.Of<IServiceProvider>();
|
||||||
|
|
||||||
|
var options = new RouteOptions();
|
||||||
|
var optionsMock = new Mock<IOptionsAccessor<RouteOptions>>();
|
||||||
|
optionsMock.SetupGet(o => o.Options).Returns(options);
|
||||||
|
|
||||||
|
return new DefaultInlineConstraintResolver(services, optionsMock.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttributeRoute CreateAttributeRoute(AttributeRouteGenerationEntry entry)
|
||||||
|
{
|
||||||
|
return CreateAttributeRoute(new StubRouter(), entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttributeRoute CreateAttributeRoute(IRouter next, AttributeRouteGenerationEntry entry)
|
||||||
|
{
|
||||||
|
return CreateAttributeRoute(next, new[] { entry });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttributeRoute CreateAttributeRoute(params AttributeRouteGenerationEntry[] entries)
|
||||||
|
{
|
||||||
|
return CreateAttributeRoute(new StubRouter(), entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteGenerationEntry[] entries)
|
||||||
|
{
|
||||||
|
return new AttributeRoute(
|
||||||
|
next,
|
||||||
|
Enumerable.Empty<AttributeRouteMatchingEntry>(),
|
||||||
|
entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class StubRouter : IRouter
|
||||||
|
{
|
||||||
|
public VirtualPathContext GenerationContext { get; set; }
|
||||||
|
|
||||||
|
public Func<VirtualPathContext, bool> GenerationDelegate { get; set; }
|
||||||
|
|
||||||
|
public RouteContext MatchingContext { get; set; }
|
||||||
|
|
||||||
|
public Func<RouteContext, bool> MatchingDelegate { get; set; }
|
||||||
|
|
||||||
|
public string GetVirtualPath(VirtualPathContext context)
|
||||||
|
{
|
||||||
|
GenerationContext = context;
|
||||||
|
|
||||||
|
if (GenerationDelegate == null)
|
||||||
|
{
|
||||||
|
context.IsBound = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.IsBound = GenerationDelegate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RouteAsync(RouteContext context)
|
||||||
|
{
|
||||||
|
if (MatchingDelegate == null)
|
||||||
|
{
|
||||||
|
context.IsHandled = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.IsHandled = MatchingDelegate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<object>(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,15 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Microsoft.AspNet.Builder;
|
using Microsoft.AspNet.Builder;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.AspNet.TestHost;
|
using Microsoft.AspNet.TestHost;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
public class RoutingTests
|
public class RoutingTests
|
||||||
|
|
@ -153,6 +156,14 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Contains("/Store/Shop/Products", result.ExpectedUrls);
|
Assert.Contains("/Store/Shop/Products", result.ExpectedUrls);
|
||||||
Assert.Equal("Store", result.Controller);
|
Assert.Equal("Store", result.Controller);
|
||||||
Assert.Equal("ListProducts", result.Action);
|
Assert.Equal("ListProducts", result.Action);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
new KeyValuePair<string, object>("controller", "Store"),
|
||||||
|
result.RouteValues);
|
||||||
|
|
||||||
|
Assert.Contains(
|
||||||
|
new KeyValuePair<string, object>("action", "ListProducts"),
|
||||||
|
result.RouteValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The url would be /Store/ListProducts with conventional routes
|
// The url would be /Store/ListProducts with conventional routes
|
||||||
|
|
@ -191,15 +202,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Contains("/Home/About", result.ExpectedUrls);
|
Assert.Contains("/Home/About", result.ExpectedUrls);
|
||||||
Assert.Equal("Store", result.Controller);
|
Assert.Equal("Store", result.Controller);
|
||||||
Assert.Equal("About", result.Action);
|
Assert.Equal("About", result.Action);
|
||||||
|
|
||||||
// A convention-routed action would have values for action and controller.
|
|
||||||
Assert.None(
|
|
||||||
result.RouteValues,
|
|
||||||
(kvp) => string.Equals(kvp.Key, "action", StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
Assert.None(
|
|
||||||
result.RouteValues,
|
|
||||||
(kvp) => string.Equals(kvp.Key, "controller", StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -222,7 +224,10 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Equal("Blog", result.Controller);
|
Assert.Equal("Blog", result.Controller);
|
||||||
Assert.Equal("Edit", result.Action);
|
Assert.Equal("Edit", result.Action);
|
||||||
|
|
||||||
// This route is parameterized on {action}, but not controller.
|
Assert.Contains(
|
||||||
|
new KeyValuePair<string, object>("controller", "Blog"),
|
||||||
|
result.RouteValues);
|
||||||
|
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
new KeyValuePair<string, object>("action", "Edit"),
|
new KeyValuePair<string, object>("action", "Edit"),
|
||||||
result.RouteValues);
|
result.RouteValues);
|
||||||
|
|
@ -230,10 +235,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
new KeyValuePair<string, object>("postId", "5"),
|
new KeyValuePair<string, object>("postId", "5"),
|
||||||
result.RouteValues);
|
result.RouteValues);
|
||||||
|
|
||||||
Assert.None(
|
|
||||||
result.RouteValues,
|
|
||||||
(kvp) => string.Equals(kvp.Key, "controller", StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// There's no [HttpGet] on the action here.
|
// There's no [HttpGet] on the action here.
|
||||||
|
|
@ -310,6 +311,392 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
result.RouteValues);
|
result.RouteValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_LinkToSelf()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/api/Employee").To(new { });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Employee", result.Controller);
|
||||||
|
Assert.Equal("List", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/api/Employee", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_LinkWithAmbientController()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/api/Employee").To(new { action = "Get", id = 5 });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Employee", result.Controller);
|
||||||
|
Assert.Equal("List", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/api/Employee/5", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_LinkToAttribueRoutedController()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/api/Employee").To(new { action = "ShowPosts", controller = "Blog" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Employee", result.Controller);
|
||||||
|
Assert.Equal("List", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Blog", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_LinkToConventionalController()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/api/Employee").To(new { action = "Index", controller = "Home" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Employee", result.Controller);
|
||||||
|
Assert.Equal("List", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConventionalRoutedAction_LinkToArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/")
|
||||||
|
.To(new { action = "BuyTickets", controller = "Flight", area = "Travel" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Home", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Travel/Flight/BuyTickets", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConventionalRoutedAction_InArea_ImplicitLinkToArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/Travel/Flight").To(new { action = "BuyTickets" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Flight", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Travel/Flight/BuyTickets", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConventionalRoutedAction_InArea_ExplicitLeaveArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/Travel/Flight").To(new { action = "Index", controller = "Home", area = "" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Flight", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConventionalRoutedAction_InArea_ImplicitLeaveArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/Travel/Flight").To(new { action = "Contact", controller = "Home", });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Flight", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Home/Contact", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_LinkToArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/api/Employee")
|
||||||
|
.To(new { action = "Schedule", controller = "Rail", area = "Travel" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Employee", result.Controller);
|
||||||
|
Assert.Equal("List", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/ContosoCorp/Trains/CheckSchedule", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_InArea_ImplicitLinkToArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/ContosoCorp/Trains/CheckSchedule").To(new { action = "Index" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Rail", result.Controller);
|
||||||
|
Assert.Equal("Schedule", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/ContosoCorp/Trains", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_InArea_ExplicitLeaveArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/ContosoCorp/Trains/CheckSchedule")
|
||||||
|
.To(new { action = "Index", controller = "Home", area = "" });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Rail", result.Controller);
|
||||||
|
Assert.Equal("Schedule", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_InArea_ImplicitLeaveArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/ContosoCorp/Trains")
|
||||||
|
.To(new { action = "Contact", controller = "Home", });
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Rail", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Home/Contact", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_InArea_LinkToConventionalRoutedActionInArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/ContosoCorp/Trains")
|
||||||
|
.To(new { action = "Index", controller = "Flight", });
|
||||||
|
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Rail", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Travel/Flight", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConventionalRoutedAction_InArea_LinkToAttributeRoutedActionInArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/Travel/Flight")
|
||||||
|
.To(new { action = "Index", controller = "Rail", });
|
||||||
|
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Flight", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/ContosoCorp/Trains", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConventionalRoutedAction_InArea_LinkToAnotherArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/Travel/Flight")
|
||||||
|
.To(new { action = "ListUsers", controller = "UserManagement", area = "Admin" });
|
||||||
|
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Flight", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Admin/Users/All", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AttributeRoutedAction_InArea_LinkToAnotherArea()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_services, _app);
|
||||||
|
var client = server.Handler;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var url = LinkFrom("http://localhost/ContosoCorp/Trains")
|
||||||
|
.To(new { action = "ListUsers", controller = "UserManagement", area = "Admin" });
|
||||||
|
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
Assert.Equal(200, response.StatusCode);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var body = await response.ReadBodyAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<RoutingResult>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("Rail", result.Controller);
|
||||||
|
Assert.Equal("Index", result.Action);
|
||||||
|
|
||||||
|
Assert.Equal("/Admin/Users/All", result.Link);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinkBuilder LinkFrom(string url)
|
||||||
|
{
|
||||||
|
return new LinkBuilder(url);
|
||||||
|
}
|
||||||
|
|
||||||
// See TestResponseGenerator in RoutingWebSite for the code that generates this data.
|
// See TestResponseGenerator in RoutingWebSite for the code that generates this data.
|
||||||
private class RoutingResult
|
private class RoutingResult
|
||||||
{
|
{
|
||||||
|
|
@ -322,6 +709,44 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
public string Action { get; set; }
|
public string Action { get; set; }
|
||||||
|
|
||||||
public string Controller { get; set; }
|
public string Controller { get; set; }
|
||||||
|
|
||||||
|
public string Link { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LinkBuilder
|
||||||
|
{
|
||||||
|
public LinkBuilder(string url)
|
||||||
|
{
|
||||||
|
Url = url;
|
||||||
|
|
||||||
|
Values = new Dictionary<string, object>();
|
||||||
|
Values.Add("link", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, object> Values { get; set; }
|
||||||
|
|
||||||
|
public LinkBuilder To(object values)
|
||||||
|
{
|
||||||
|
var dictionary = new RouteValueDictionary(values);
|
||||||
|
foreach (var kvp in dictionary)
|
||||||
|
{
|
||||||
|
Values.Add("link_" + kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Url + '?' + string.Join("&", Values.Select(kvp => kvp.Key + '=' + kvp.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator string (LinkBuilder builder)
|
||||||
|
{
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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.Admin
|
||||||
|
{
|
||||||
|
[Area("Admin")]
|
||||||
|
[Route("{area}/Users")]
|
||||||
|
public class UserManagementController : Controller
|
||||||
|
{
|
||||||
|
private readonly TestResponseGenerator _generator;
|
||||||
|
|
||||||
|
public UserManagementController(TestResponseGenerator generator)
|
||||||
|
{
|
||||||
|
_generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("All")]
|
||||||
|
public IActionResult ListUsers()
|
||||||
|
{
|
||||||
|
return _generator.Generate("Admin/Users/All");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using Microsoft.AspNet.Mvc;
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
using System;
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
namespace RoutingWebSite
|
namespace RoutingWebSite
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
|
namespace RoutingWebSite.Travel
|
||||||
|
{
|
||||||
|
[Area("Travel")]
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
private readonly TestResponseGenerator _generator;
|
||||||
|
|
||||||
|
public HomeController(TestResponseGenerator generator)
|
||||||
|
{
|
||||||
|
_generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/Travel", "/Travel/Home", "/Travel/Home/Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("ContosoCorp/AboutTravel")]
|
||||||
|
public IActionResult About()
|
||||||
|
{
|
||||||
|
return _generator.Generate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
[Area("Travel")]
|
||||||
|
[Route("ContosoCorp/Trains")]
|
||||||
|
public class RailController
|
||||||
|
{
|
||||||
|
private readonly TestResponseGenerator _generator;
|
||||||
|
|
||||||
|
public RailController(TestResponseGenerator generator)
|
||||||
|
{
|
||||||
|
_generator = generator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/ContosoCorp/Trains");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("CheckSchedule")]
|
||||||
|
public IActionResult Schedule()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/ContosoCorp/Trains/Schedule");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,5 +25,10 @@ namespace RoutingWebSite
|
||||||
// There are no urls that reach this action - it's hidden by an attribute route.
|
// There are no urls that reach this action - it's hidden by an attribute route.
|
||||||
return _generator.Generate();
|
return _generator.Generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IActionResult Contact()
|
||||||
|
{
|
||||||
|
return _generator.Generate("/Home/Contact");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +28,10 @@
|
||||||
<DevelopmentServerPort>11178</DevelopmentServerPort>
|
<DevelopmentServerPort>11178</DevelopmentServerPort>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Areas\Admin\UserManagementController.cs" />
|
||||||
<Compile Include="Areas\Travel\FlightController.cs" />
|
<Compile Include="Areas\Travel\FlightController.cs" />
|
||||||
|
<Compile Include="Areas\Travel\HomeController.cs" />
|
||||||
|
<Compile Include="Areas\Travel\RailController.cs" />
|
||||||
<Compile Include="Controllers\BlogController.cs" />
|
<Compile Include="Controllers\BlogController.cs" />
|
||||||
<Compile Include="Controllers\EmployeeController.cs" />
|
<Compile Include="Controllers\EmployeeController.cs" />
|
||||||
<Compile Include="Controllers\HomeController.cs" />
|
<Compile Include="Controllers\HomeController.cs" />
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNet.Mvc;
|
using Microsoft.AspNet.Mvc;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -23,6 +24,18 @@ namespace RoutingWebSite
|
||||||
|
|
||||||
public JsonResult Generate(params string[] expectedUrls)
|
public JsonResult Generate(params string[] expectedUrls)
|
||||||
{
|
{
|
||||||
|
var link = (string)null;
|
||||||
|
var query = _actionContext.HttpContext.Request.Query;
|
||||||
|
if (query.ContainsKey("link"))
|
||||||
|
{
|
||||||
|
var values = query
|
||||||
|
.Where(kvp => kvp.Key != "link" && kvp.Key != "link_action" && kvp.Key != "link_controller")
|
||||||
|
.ToDictionary(kvp => kvp.Key.Substring("link_".Length), kvp => (object)kvp.Value[0]);
|
||||||
|
|
||||||
|
var urlHelper = _actionContext.HttpContext.RequestServices.GetService<IUrlHelper>();
|
||||||
|
link = urlHelper.Action(query["link_action"], query["link_controller"], values);
|
||||||
|
}
|
||||||
|
|
||||||
return new JsonResult(new
|
return new JsonResult(new
|
||||||
{
|
{
|
||||||
expectedUrls = expectedUrls,
|
expectedUrls = expectedUrls,
|
||||||
|
|
@ -31,6 +44,8 @@ namespace RoutingWebSite
|
||||||
|
|
||||||
action = _actionContext.ActionDescriptor.Name,
|
action = _actionContext.ActionDescriptor.Name,
|
||||||
controller = ((ReflectedActionDescriptor)_actionContext.ActionDescriptor).ControllerDescriptor.Name,
|
controller = ((ReflectedActionDescriptor)_actionContext.ActionDescriptor).ControllerDescriptor.Name,
|
||||||
|
|
||||||
|
link,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue