Adding Attribute Routing Link Generation

This commit is contained in:
Ryan Nowak 2014-06-10 14:16:13 -07:00
parent 340bd7550a
commit 745239f09f
19 changed files with 1298 additions and 74 deletions

View File

@ -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);
}
}
}

View File

@ -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; }

View File

@ -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" />

View File

@ -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>>();

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}
}

View File

@ -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; }
}
}
}
}

View File

@ -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" />

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}
}
}

View File

@ -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");
}
}
}

View File

@ -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
{

View File

@ -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();
}
}
}

View File

@ -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");
}
}
}

View File

@ -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");
}
}
}

View File

@ -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" />

View File

@ -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,
});
}
}