Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Adding Attribute Routing Link Generation
Browse files Browse the repository at this point in the history
  • Loading branch information
rynowak committed Jul 21, 2014
1 parent 340bd75 commit 745239f
Show file tree
Hide file tree
Showing 19 changed files with 1,296 additions and 72 deletions.
6 changes: 6 additions & 0 deletions samples/MvcSample.Web/SimpleRest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,11 @@ public string GetOtherThing()
{
return "Get other thing";
}

[HttpGet("Link")]
public string GenerateLink(string action = null, string controller = null)
{
return Url.Action(action, controller);
}
}
}
8 changes: 6 additions & 2 deletions src/Microsoft.AspNet.Mvc.Core/ActionDescriptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -13,7 +12,12 @@ public class ActionDescriptor
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; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/MvcRouteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ public async Task RouteAsync([NotNull] RouteContext context)
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>>();
Expand Down
57 changes: 41 additions & 16 deletions src/Microsoft.AspNet.Mvc.Core/ReflectedActionDescriptorProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,26 +211,41 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
}
else
{
// An attribute routed action will ignore conventional routed constraints.
actionDescriptor.RouteConstraints.Clear();
// 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)
{
ambientValues.Add(constraint.RouteKey, constraint.RouteValue);
}

actionDescriptor.RouteValues = ambientValues;

// 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.
// 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);
if (template.Parameters.Any(
p => p.IsParameter &&
string.Equals(p.Name, "action", StringComparison.OrdinalIgnoreCase)))

var routeConstraints = new List<RouteDataActionConstraint>();
foreach (var constraint in actionDescriptor.RouteConstraints)
{
actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(
"action",
action.ActionName));
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;
}
}
Expand All @@ -250,11 +265,21 @@ public List<ReflectedActionDescriptor> Build(ReflectedApplicationModel model)
{
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);
}
}
}
}
Expand Down Expand Up @@ -282,4 +307,4 @@ private static Dictionary<string, string> GetRouteGroupsByTemplate(ReflectedAppl
return groupsByTemplate;
}
}
}
}
143 changes: 135 additions & 8 deletions src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand All @@ -47,9 +56,127 @@ public async Task RouteAsync([NotNull] RouteContext context)
/// <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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading

0 comments on commit 745239f

Please sign in to comment.