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";
|
||||
}
|
||||
|
||||
[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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Mvc.Routing;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc
|
||||
{
|
||||
|
|
@ -13,7 +12,12 @@ namespace Microsoft.AspNet.Mvc
|
|||
public List<RouteDataActionConstraint> RouteConstraints { get; set; }
|
||||
|
||||
/// <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>
|
||||
public string RouteTemplate { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -217,7 +217,8 @@
|
|||
<Compile Include="KnownRouteValueConstraint.cs" />
|
||||
<Compile Include="RouteKeyHandling.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\AttributeRouteTemplate.cs" />
|
||||
<Compile Include="Routing\AttributeRouting.cs" />
|
||||
|
|
|
|||
|
|
@ -38,6 +38,17 @@ namespace Microsoft.AspNet.Mvc
|
|||
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 contextAccessor = services.GetService<IContextAccessor<ActionContext>>();
|
||||
|
|
|
|||
|
|
@ -211,26 +211,41 @@ namespace Microsoft.AspNet.Mvc
|
|||
}
|
||||
else
|
||||
{
|
||||
// An attribute routed action will ignore conventional routed constraints.
|
||||
actionDescriptor.RouteConstraints.Clear();
|
||||
|
||||
// TODO #738 - this currently has parity with what we did in MVC5 for the action
|
||||
// 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)))
|
||||
// An attribute routed action will ignore conventional routed constraints. We still
|
||||
// want to provide these values as ambient values.
|
||||
var ambientValues = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var constraint in actionDescriptor.RouteConstraints)
|
||||
{
|
||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||
"action",
|
||||
action.ActionName));
|
||||
ambientValues.Add(constraint.RouteKey, constraint.RouteValue);
|
||||
}
|
||||
|
||||
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];
|
||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||
routeConstraints.Add(new RouteDataActionConstraint(
|
||||
AttributeRouting.RouteGroupKey,
|
||||
routeGroup));
|
||||
|
||||
actionDescriptor.RouteConstraints = routeConstraints;
|
||||
|
||||
actionDescriptor.RouteTemplate = templateText;
|
||||
}
|
||||
}
|
||||
|
|
@ -250,11 +265,21 @@ namespace Microsoft.AspNet.Mvc
|
|||
{
|
||||
foreach (var key in removalConstraints)
|
||||
{
|
||||
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
|
||||
if (actionDescriptor.RouteTemplate == null)
|
||||
{
|
||||
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
|
||||
key,
|
||||
RouteKeyHandling.DenyKey));
|
||||
if (!HasConstraint(actionDescriptor.RouteConstraints, key))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// 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;
|
||||
|
|
@ -15,26 +16,34 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
public class AttributeRoute : IRouter
|
||||
{
|
||||
private readonly IRouter _next;
|
||||
private readonly TemplateRoute[] _routes;
|
||||
private readonly TemplateRoute[] _matchingRoutes;
|
||||
private readonly AttributeRouteGenerationEntry[] _generationEntries;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="AttributeRoute"/>.
|
||||
/// </summary>
|
||||
/// <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] IEnumerable<AttributeRouteEntry> entries)
|
||||
public AttributeRoute(
|
||||
[NotNull] IRouter next,
|
||||
[NotNull] IEnumerable<AttributeRouteMatchingEntry> matchingEntries,
|
||||
[NotNull] IEnumerable<AttributeRouteGenerationEntry> generationEntries)
|
||||
{
|
||||
_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.
|
||||
_routes = entries.OrderBy(e => e.Precedence).Select(e => e.Route).ToArray();
|
||||
// a good data-structure here. See #740
|
||||
_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 />
|
||||
public async Task RouteAsync([NotNull] RouteContext context)
|
||||
{
|
||||
foreach (var route in _routes)
|
||||
foreach (var route in _matchingRoutes)
|
||||
{
|
||||
await route.RouteAsync(context);
|
||||
if (context.IsHandled)
|
||||
|
|
@ -47,9 +56,127 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// <inheritdoc />
|
||||
public string GetVirtualPath([NotNull] VirtualPathContext context)
|
||||
{
|
||||
// Not implemented right now, but we don't want to throw here and block other routes from generating
|
||||
// a link.
|
||||
// To generate a link, we iterate the collection of entries (in order of precedence) and execute
|
||||
// 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;
|
||||
}
|
||||
|
||||
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.
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual route that will be aggregated
|
||||
/// into the <see cref="AttributeRoute"/>.
|
||||
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual URL-matching route that will be
|
||||
/// aggregated into the <see cref="AttributeRoute"/>.
|
||||
/// </summary>
|
||||
public class AttributeRouteEntry
|
||||
public class AttributeRouteMatchingEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// The precedence of the template.
|
||||
|
|
@ -21,4 +22,4 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
/// </summary>
|
||||
public TemplateRoute Route { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,38 +26,57 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
{
|
||||
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 routeInfos = GetRouteInfos(actions, inlineConstraintResolver);
|
||||
|
||||
var entries = new List<AttributeRouteEntry>();
|
||||
foreach (var routeGroup in routeTemplatesByGroup)
|
||||
// We're creating one AttributeRouteGenerationEntry per action. This allows us to match the intended
|
||||
// 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 template = routeGroup.Value;
|
||||
var defaults = routeInfo.ParsedTemplate.Parameters
|
||||
.Where(p => p.DefaultValue != null)
|
||||
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var parsedTemplate = TemplateParser.Parse(template, inlineConstraintResolver);
|
||||
var precedence = AttributeRoutePrecedence.Compute(parsedTemplate);
|
||||
var constraints = routeInfo.ParsedTemplate.Parameters
|
||||
.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(
|
||||
target,
|
||||
template,
|
||||
routeInfo.RouteTemplate,
|
||||
defaults: new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ RouteGroupKey, routeGroupId },
|
||||
{ RouteGroupKey, routeInfo.RouteGroup },
|
||||
},
|
||||
constraints: null,
|
||||
inlineConstraintResolver: inlineConstraintResolver),
|
||||
});
|
||||
}
|
||||
|
||||
return new AttributeRoute(target, entries);
|
||||
return new AttributeRoute(target, matchingEntries, generationEntries);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ActionDescriptor> GetActionDescriptors(IServiceProvider services)
|
||||
|
|
@ -68,9 +87,27 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
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))
|
||||
{
|
||||
var constraint = action.RouteConstraints
|
||||
|
|
@ -84,14 +121,31 @@ namespace Microsoft.AspNet.Mvc.Routing
|
|||
continue;
|
||||
}
|
||||
|
||||
var routeGroup = constraint.RouteValue;
|
||||
if (!routeTemplatesByGroup.ContainsKey(routeGroup))
|
||||
var parsedTemplate = TemplateParser.Parse(action.RouteTemplate, constraintResolver);
|
||||
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\AttributeRouteTemplateTests.cs" />
|
||||
<Compile Include="StaticControllerAssemblyProvider.cs" />
|
||||
<Compile Include="Routing\AttributeRouteTests.cs" />
|
||||
<Compile Include="TestController.cs" />
|
||||
<Compile Include="TypeHelperTest.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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNet.Builder;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.TestHost;
|
||||
using Xunit;
|
||||
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||
{
|
||||
public class RoutingTests
|
||||
|
|
@ -153,6 +156,14 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Contains("/Store/Shop/Products", result.ExpectedUrls);
|
||||
Assert.Equal("Store", result.Controller);
|
||||
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
|
||||
|
|
@ -191,15 +202,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Contains("/Home/About", result.ExpectedUrls);
|
||||
Assert.Equal("Store", result.Controller);
|
||||
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]
|
||||
|
|
@ -222,7 +224,10 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Equal("Blog", result.Controller);
|
||||
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(
|
||||
new KeyValuePair<string, object>("action", "Edit"),
|
||||
result.RouteValues);
|
||||
|
|
@ -230,10 +235,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
Assert.Contains(
|
||||
new KeyValuePair<string, object>("postId", "5"),
|
||||
result.RouteValues);
|
||||
|
||||
Assert.None(
|
||||
result.RouteValues,
|
||||
(kvp) => string.Equals(kvp.Key, "controller", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// There's no [HttpGet] on the action here.
|
||||
|
|
@ -310,6 +311,392 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
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.
|
||||
private class RoutingResult
|
||||
{
|
||||
|
|
@ -322,6 +709,44 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
|||
public string Action { 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;
|
||||
using System;
|
||||
// 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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
return _generator.Generate();
|
||||
}
|
||||
|
||||
public IActionResult Contact()
|
||||
{
|
||||
return _generator.Generate("/Home/Contact");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,10 @@
|
|||
<DevelopmentServerPort>11178</DevelopmentServerPort>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Areas\Admin\UserManagementController.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\EmployeeController.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.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
|
||||
|
|
@ -23,6 +24,18 @@ namespace RoutingWebSite
|
|||
|
||||
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
|
||||
{
|
||||
expectedUrls = expectedUrls,
|
||||
|
|
@ -31,6 +44,8 @@ namespace RoutingWebSite
|
|||
|
||||
action = _actionContext.ActionDescriptor.Name,
|
||||
controller = ((ReflectedActionDescriptor)_actionContext.ActionDescriptor).ControllerDescriptor.Name,
|
||||
|
||||
link,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue