Makes attribute routing respond to AD updates

This change makes the attribute route capable of responding to updates in
the action descriptor collection.

The change moves the actual route implementation to InnerAttributeRoute.
AttributeRoute now wraps an InnerAttributeRoute - when processing a route,
AttributeRoute will now check with action descriptor collection if its
InnerAttributeRoute is still 'current' and rebuild it if necessary.
This commit is contained in:
Ryan Nowak 2015-01-29 11:13:40 -08:00
parent 09b0aa5182
commit b4d1eec87a
8 changed files with 2392 additions and 2176 deletions

View File

@ -3,274 +3,306 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Internal.Routing;
using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc.Routing
{
/// <summary>
/// An <see cref="IRouter"/> implementation for attribute routing.
/// </summary>
public class AttributeRoute : IRouter
{
private readonly IRouter _next;
private readonly LinkGenerationDecisionTree _linkGenerationTree;
private readonly TemplateRoute[] _matchingRoutes;
private readonly IDictionary<string, AttributeRouteLinkGenerationEntry> _namedEntries;
private readonly IRouter _target;
private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
private readonly IInlineConstraintResolver _constraintResolver;
private ILogger _logger;
private ILogger _constraintLogger;
// These loggers are used by the inner route, keep them around to avoid re-creating.
private readonly ILogger _routeLogger;
private readonly ILogger _constraintLogger;
private InnerAttributeRoute _inner;
/// <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<AttributeRouteMatchingEntry> matchingEntries,
[NotNull] IEnumerable<AttributeRouteLinkGenerationEntry> linkGenerationEntries,
[NotNull] ILoggerFactory factory)
[NotNull] IRouter target,
[NotNull] IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider,
[NotNull] IInlineConstraintResolver constraintResolver,
[NotNull] ILoggerFactory loggerFactory)
{
_next = next;
_target = target;
_actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider;
_constraintResolver = constraintResolver;
// Order all the entries by order, then precedence, and then finally by template in order to provide
// a stable routing and link generation order for templates with same order and precedence.
// We use ordinal comparison for the templates because we only care about them being exactly equal and
// we don't want to make any equivalence between templates based on the culture of the machine.
_routeLogger = loggerFactory.Create<InnerAttributeRoute>();
_constraintLogger = loggerFactory.Create(typeof(RouteConstraintMatcher).FullName);
_matchingRoutes = matchingEntries
.OrderBy(o => o.Order)
.ThenBy(e => e.Precedence)
.ThenBy(e => e.Route.RouteTemplate, StringComparer.Ordinal)
.Select(e => e.Route)
.ToArray();
var namedEntries = new Dictionary<string, AttributeRouteLinkGenerationEntry>(
StringComparer.OrdinalIgnoreCase);
foreach (var entry in linkGenerationEntries)
{
// Skip unnamed entries
if (entry.Name == null)
{
continue;
}
// We only need to keep one AttributeRouteLinkGenerationEntry per route template
// so in case two entries have the same name and the same template we only keep
// the first entry.
AttributeRouteLinkGenerationEntry namedEntry = null;
if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
!namedEntry.TemplateText.Equals(entry.TemplateText, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name),
"linkGenerationEntries");
}
else if (namedEntry == null)
{
namedEntries.Add(entry.Name, entry);
}
}
_namedEntries = namedEntries;
// The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
_logger = factory.Create<AttributeRoute>();
_constraintLogger = factory.Create(typeof(RouteConstraintMatcher).FullName);
// Force creation of the route to report issues on startup.
GetInnerRoute();
}
/// <inheritdoc />
public async Task RouteAsync([NotNull] RouteContext context)
public string GetVirtualPath(VirtualPathContext context)
{
using (_logger.BeginScope("AttributeRoute.RouteAsync"))
var route = GetInnerRoute();
return route.GetVirtualPath(context);
}
/// <inheritdoc />
public Task RouteAsync(RouteContext context)
{
var route = GetInnerRoute();
return route.RouteAsync(context);
}
private InnerAttributeRoute GetInnerRoute()
{
var actions = _actionDescriptorsCollectionProvider.ActionDescriptors;
// This is a safe-race. We'll never set inner back to null after initializing
// it on startup.
if (_inner == null || _inner.Version != actions.Version)
{
foreach (var route in _matchingRoutes)
{
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(route);
try
{
context.RouteData = newRouteData;
await route.RouteAsync(context);
}
finally
{
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
if (context.IsHandled)
{
break;
}
}
_inner = BuildRoute(actions);
}
if (_logger.IsEnabled(LogLevel.Verbose))
return _inner;
}
private InnerAttributeRoute BuildRoute(ActionDescriptorsCollection actions)
{
var routeInfos = GetRouteInfos(_constraintResolver, actions.Items);
// 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<AttributeRouteLinkGenerationEntry>();
foreach (var routeInfo in routeInfos)
{
_logger.WriteValues(new AttributeRouteRouteAsyncValues()
generationEntries.Add(new AttributeRouteLinkGenerationEntry()
{
MatchingRoutes = _matchingRoutes,
Handled = context.IsHandled
Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults),
Defaults = routeInfo.Defaults,
Constraints = routeInfo.Constraints,
Order = routeInfo.Order,
Precedence = routeInfo.Precedence,
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
RouteGroup = routeInfo.RouteGroup,
Template = routeInfo.ParsedTemplate,
TemplateText = routeInfo.RouteTemplate,
Name = routeInfo.Name,
});
}
}
/// <inheritdoc />
public string GetVirtualPath([NotNull] VirtualPathContext context)
{
// If it's a named route we will try to generate a link directly and
// if we can't, we will not try to generate it using an unnamed route.
if (context.RouteName != null)
// 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)
{
return GetVirtualPathForNamedRoute(context);
matchingEntries.Add(new AttributeRouteMatchingEntry()
{
Order = routeInfo.Order,
Precedence = routeInfo.Precedence,
Route = new TemplateRoute(
_target,
routeInfo.RouteTemplate,
defaults: new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ AttributeRouting.RouteGroupKey, routeInfo.RouteGroup },
},
constraints: null,
dataTokens: null,
inlineConstraintResolver: _constraintResolver),
});
}
// The decision tree will give us back all entries that match the provided route data in the correct
// order. We just need to iterate them and use the first one that can generate a link.
var matches = _linkGenerationTree.GetMatches(context);
return new InnerAttributeRoute(
_target,
matchingEntries,
generationEntries,
_routeLogger,
_constraintLogger,
actions.Version);
}
foreach (var entry in matches)
private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos)
{
var routeInfosByGroupId = new Dictionary<string, RouteInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var routeInfo in routeInfos)
{
var path = GenerateLink(context, entry);
if (path != null)
if (!routeInfosByGroupId.ContainsKey(routeInfo.RouteGroup))
{
context.IsBound = true;
return path;
routeInfosByGroupId.Add(routeInfo.RouteGroup, routeInfo);
}
}
return null;
return routeInfosByGroupId.Values;
}
private string GetVirtualPathForNamedRoute(VirtualPathContext context)
private static List<RouteInfo> GetRouteInfos(
IInlineConstraintResolver constraintResolver,
IReadOnlyList<ActionDescriptor> actions)
{
AttributeRouteLinkGenerationEntry entry;
if (_namedEntries.TryGetValue(context.RouteName, out entry))
var routeInfos = new List<RouteInfo>();
var errors = new List<RouteInfo>();
// This keeps a cache of 'Template' objects. It's a fairly common case that multiple actions
// will use the same route template string; thus, the `Template` object can be shared.
//
// For a relatively simple route template, the `Template` object will hold about 500 bytes
// of memory, so sharing is worthwhile.
var templateCache = new Dictionary<string, RouteTemplate>(StringComparer.OrdinalIgnoreCase);
var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo != null &&
a.AttributeRouteInfo.Template != null);
foreach (var action in attributeRoutedActions)
{
var path = GenerateLink(context, entry);
if (path != null)
var routeInfo = GetRouteInfo(constraintResolver, templateCache, action);
if (routeInfo.ErrorMessage == null)
{
context.IsBound = true;
return path;
routeInfos.Add(routeInfo);
}
else
{
errors.Add(routeInfo);
}
}
return null;
if (errors.Count > 0)
{
var allErrors = string.Join(
Environment.NewLine + Environment.NewLine,
errors.Select(
e => Resources.FormatAttributeRoute_IndividualErrorMessage(
e.ActionDescriptor.DisplayName,
Environment.NewLine,
e.ErrorMessage)));
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(Environment.NewLine, allErrors);
throw new InvalidOperationException(message);
}
return routeInfos;
}
private string GenerateLink(VirtualPathContext context, AttributeRouteLinkGenerationEntry entry)
private static RouteInfo GetRouteInfo(
IInlineConstraintResolver constraintResolver,
Dictionary<string, RouteTemplate> templateCache,
ActionDescriptor action)
{
// 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)
var constraint = action.RouteConstraints
.Where(c => c.RouteKey == AttributeRouting.RouteGroupKey)
.FirstOrDefault();
if (constraint == null ||
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
constraint.RouteValue == null)
{
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.
// This can happen if an ActionDescriptor has a route template, but doesn't have one of our
// special route group constraints. This is a good indication that the user is using a 3rd party
// routing system, or has customized their ADs in a way that we can no longer understand them.
//
// We just treat this case as an 'opt-out' of our attribute routing system.
return null;
}
var matched = RouteConstraintMatcher.Match(
entry.Constraints,
bindingResult.CombinedValues,
context.Context,
this,
RouteDirection.UrlGeneration,
_constraintLogger);
if (!matched)
var routeInfo = new RouteInfo()
{
// A constraint 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,
ActionDescriptor = action,
RouteGroup = constraint.RouteValue,
RouteTemplate = action.AttributeRouteInfo.Template,
};
var path = _next.GetVirtualPath(childContext);
if (path != null)
try
{
// 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))
{
// If the required value is an 'empty' route value, then ignore ambient values.
// This handles a case where we're generating a link to an action like:
// { area = "", controller = "Home", action = "Index" }
//
// and the ambient values has a value for area.
if (value != null)
RouteTemplate parsedTemplate;
if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate))
{
context.AmbientValues.TryGetValue(key, out providedValue);
// Parsing with throw if the template is invalid.
parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template);
templateCache.Add(action.AttributeRouteInfo.Template, parsedTemplate);
}
routeInfo.ParsedTemplate = parsedTemplate;
}
catch (Exception ex)
{
routeInfo.ErrorMessage = ex.Message;
return routeInfo;
}
foreach (var kvp in action.RouteValueDefaults)
{
foreach (var parameter in routeInfo.ParsedTemplate.Parameters)
{
if (string.Equals(kvp.Key, parameter.Name, StringComparison.OrdinalIgnoreCase))
{
routeInfo.ErrorMessage = Resources.FormatAttributeRoute_CannotContainParameter(
routeInfo.RouteTemplate,
kvp.Key,
kvp.Value);
return routeInfo;
}
}
}
return TemplateBinder.RoutePartsEqual(providedValue, value);
routeInfo.Order = action.AttributeRouteInfo.Order;
routeInfo.Precedence = AttributeRoutePrecedence.Compute(routeInfo.ParsedTemplate);
routeInfo.Name = action.AttributeRouteInfo.Name;
var constraintBuilder = new RouteConstraintBuilder(constraintResolver, routeInfo.RouteTemplate);
foreach (var parameter in routeInfo.ParsedTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
}
routeInfo.Constraints = constraintBuilder.Build();
routeInfo.Defaults = routeInfo.ParsedTemplate.Parameters
.Where(p => p.DefaultValue != null)
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase);
return routeInfo;
}
private class RouteInfo
{
public ActionDescriptor ActionDescriptor { get; set; }
public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
public IReadOnlyDictionary<string, object> Defaults { get; set; }
public string ErrorMessage { get; set; }
public RouteTemplate ParsedTemplate { get; set; }
public int Order { get; set; }
public decimal Precedence { get; set; }
public string RouteGroup { get; set; }
public string RouteTemplate { get; set; }
public string Name { get; set; }
}
}
}
}

View File

@ -8,8 +8,8 @@ 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"/>.
/// Used to build an <see cref="InnerAttributeRoute"/>. Represents an individual URL-generating route that will be
/// aggregated into the <see cref="InnerAttributeRoute"/>.
/// </summary>
public class AttributeRouteLinkGenerationEntry
{

View File

@ -6,8 +6,8 @@ using Microsoft.AspNet.Routing.Template;
namespace Microsoft.AspNet.Mvc.Routing
{
/// <summary>
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual URL-matching route that will be
/// aggregated into the <see cref="AttributeRoute"/>.
/// Used to build an <see cref="InnerAttributeRoute"/>. Represents an individual URL-matching route that will be
/// aggregated into the <see cref="InnerAttributeRoute"/>.
/// </summary>
public class AttributeRouteMatchingEntry
{

View File

@ -2,11 +2,7 @@
// 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 Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
@ -25,247 +21,12 @@ namespace Microsoft.AspNet.Mvc.Routing
/// <param name="services">The application services.</param>
/// <returns>An attribute route.</returns>
public static IRouter CreateAttributeMegaRoute([NotNull] IRouter target, [NotNull] IServiceProvider services)
{
var actions = GetActionDescriptors(services);
var inlineConstraintResolver = services.GetRequiredService<IInlineConstraintResolver>();
var routeInfos = GetRouteInfos(inlineConstraintResolver, actions);
// 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<AttributeRouteLinkGenerationEntry>();
foreach (var routeInfo in routeInfos)
{
generationEntries.Add(new AttributeRouteLinkGenerationEntry()
{
Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults),
Defaults = routeInfo.Defaults,
Constraints = routeInfo.Constraints,
Order = routeInfo.Order,
Precedence = routeInfo.Precedence,
RequiredLinkValues = routeInfo.ActionDescriptor.RouteValueDefaults,
RouteGroup = routeInfo.RouteGroup,
Template = routeInfo.ParsedTemplate,
TemplateText = routeInfo.RouteTemplate,
Name = routeInfo.Name,
});
}
// 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()
{
Order = routeInfo.Order,
Precedence = routeInfo.Precedence,
Route = new TemplateRoute(
target,
routeInfo.RouteTemplate,
defaults: new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ RouteGroupKey, routeInfo.RouteGroup },
},
constraints: null,
dataTokens: null,
inlineConstraintResolver: inlineConstraintResolver),
});
}
return new AttributeRoute(
target,
matchingEntries,
generationEntries,
services.GetRequiredService<ILoggerFactory>());
}
private static IReadOnlyList<ActionDescriptor> GetActionDescriptors(IServiceProvider services)
{
var actionDescriptorProvider = services.GetRequiredService<IActionDescriptorsCollectionProvider>();
var inlineConstraintResolver = services.GetRequiredService<IInlineConstraintResolver>();
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var actionDescriptorsCollection = actionDescriptorProvider.ActionDescriptors;
return actionDescriptorsCollection.Items;
}
private static IEnumerable<RouteInfo> GroupRouteInfosByGroupId(List<RouteInfo> routeInfos)
{
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(
IInlineConstraintResolver constraintResolver,
IReadOnlyList<ActionDescriptor> actions)
{
var routeInfos = new List<RouteInfo>();
var errors = new List<RouteInfo>();
// This keeps a cache of 'Template' objects. It's a fairly common case that multiple actions
// will use the same route template string; thus, the `Template` object can be shared.
//
// For a relatively simple route template, the `Template` object will hold about 500 bytes
// of memory, so sharing is worthwhile.
var templateCache = new Dictionary<string, RouteTemplate>(StringComparer.OrdinalIgnoreCase);
var attributeRoutedActions = actions.Where(a => a.AttributeRouteInfo != null &&
a.AttributeRouteInfo.Template != null);
foreach (var action in attributeRoutedActions)
{
var routeInfo = GetRouteInfo(constraintResolver, templateCache, action);
if (routeInfo.ErrorMessage == null)
{
routeInfos.Add(routeInfo);
}
else
{
errors.Add(routeInfo);
}
}
if (errors.Count > 0)
{
var allErrors = string.Join(
Environment.NewLine + Environment.NewLine,
errors.Select(
e => Resources.FormatAttributeRoute_IndividualErrorMessage(
e.ActionDescriptor.DisplayName,
Environment.NewLine,
e.ErrorMessage)));
var message = Resources.FormatAttributeRoute_AggregateErrorMessage(Environment.NewLine, allErrors);
throw new InvalidOperationException(message);
}
return routeInfos;
}
private static RouteInfo GetRouteInfo(
IInlineConstraintResolver constraintResolver,
Dictionary<string, RouteTemplate> templateCache,
ActionDescriptor action)
{
var constraint = action.RouteConstraints
.Where(c => c.RouteKey == AttributeRouting.RouteGroupKey)
.FirstOrDefault();
if (constraint == null ||
constraint.KeyHandling != RouteKeyHandling.RequireKey ||
constraint.RouteValue == null)
{
// This can happen if an ActionDescriptor has a route template, but doesn't have one of our
// special route group constraints. This is a good indication that the user is using a 3rd party
// routing system, or has customized their ADs in a way that we can no longer understand them.
//
// We just treat this case as an 'opt-out' of our attribute routing system.
return null;
}
var routeInfo = new RouteInfo()
{
ActionDescriptor = action,
RouteGroup = constraint.RouteValue,
RouteTemplate = action.AttributeRouteInfo.Template,
};
try
{
RouteTemplate parsedTemplate;
if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate))
{
// Parsing with throw if the template is invalid.
parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template);
templateCache.Add(action.AttributeRouteInfo.Template, parsedTemplate);
}
routeInfo.ParsedTemplate = parsedTemplate;
}
catch (Exception ex)
{
routeInfo.ErrorMessage = ex.Message;
return routeInfo;
}
foreach (var kvp in action.RouteValueDefaults)
{
foreach (var parameter in routeInfo.ParsedTemplate.Parameters)
{
if (string.Equals(kvp.Key, parameter.Name, StringComparison.OrdinalIgnoreCase))
{
routeInfo.ErrorMessage = Resources.FormatAttributeRoute_CannotContainParameter(
routeInfo.RouteTemplate,
kvp.Key,
kvp.Value);
return routeInfo;
}
}
}
routeInfo.Order = action.AttributeRouteInfo.Order;
routeInfo.Precedence = AttributeRoutePrecedence.Compute(routeInfo.ParsedTemplate);
routeInfo.Name = action.AttributeRouteInfo.Name;
var constraintBuilder = new RouteConstraintBuilder(constraintResolver, routeInfo.RouteTemplate);
foreach (var parameter in routeInfo.ParsedTemplate.Parameters)
{
if (parameter.InlineConstraints != null)
{
if (parameter.IsOptional)
{
constraintBuilder.SetOptional(parameter.Name);
}
foreach (var inlineConstraint in parameter.InlineConstraints)
{
constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint);
}
}
}
routeInfo.Constraints = constraintBuilder.Build();
routeInfo.Defaults = routeInfo.ParsedTemplate.Parameters
.Where(p => p.DefaultValue != null)
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase);
return routeInfo;
}
private class RouteInfo
{
public ActionDescriptor ActionDescriptor { get; set; }
public IReadOnlyDictionary<string, IRouteConstraint> Constraints { get; set; }
public IReadOnlyDictionary<string, object> Defaults { get; set; }
public string ErrorMessage { get; set; }
public RouteTemplate ParsedTemplate { get; set; }
public int Order { get; set; }
public decimal Precedence { get; set; }
public string RouteGroup { get; set; }
public string RouteTemplate { get; set; }
public string Name { get; set; }
return new AttributeRoute(target, actionDescriptorProvider, inlineConstraintResolver, loggerFactory);
}
}
}

View File

@ -0,0 +1,285 @@
// 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.Mvc.Core;
using Microsoft.AspNet.Mvc.Internal.Routing;
using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc.Routing
{
/// <summary>
/// An <see cref="IRouter"/> implementation for attribute routing.
/// </summary>
public class InnerAttributeRoute : IRouter
{
private readonly IRouter _next;
private readonly LinkGenerationDecisionTree _linkGenerationTree;
private readonly TemplateRoute[] _matchingRoutes;
private readonly IDictionary<string, AttributeRouteLinkGenerationEntry> _namedEntries;
private ILogger _logger;
private ILogger _constraintLogger;
/// <summary>
/// Creates a new <see cref="InnerAttributeRoute"/>.
/// </summary>
/// <param name="next">The next router. Invoked when a route entry matches.</param>
/// <param name="entries">The set of route entries.</param>
public InnerAttributeRoute(
[NotNull] IRouter next,
[NotNull] IEnumerable<AttributeRouteMatchingEntry> matchingEntries,
[NotNull] IEnumerable<AttributeRouteLinkGenerationEntry> linkGenerationEntries,
[NotNull] ILogger logger,
[NotNull] ILogger constraintLogger,
int version)
{
_next = next;
_logger = logger;
_constraintLogger = constraintLogger;
Version = version;
// Order all the entries by order, then precedence, and then finally by template in order to provide
// a stable routing and link generation order for templates with same order and precedence.
// We use ordinal comparison for the templates because we only care about them being exactly equal and
// we don't want to make any equivalence between templates based on the culture of the machine.
_matchingRoutes = matchingEntries
.OrderBy(o => o.Order)
.ThenBy(e => e.Precedence)
.ThenBy(e => e.Route.RouteTemplate, StringComparer.Ordinal)
.Select(e => e.Route)
.ToArray();
var namedEntries = new Dictionary<string, AttributeRouteLinkGenerationEntry>(
StringComparer.OrdinalIgnoreCase);
foreach (var entry in linkGenerationEntries)
{
// Skip unnamed entries
if (entry.Name == null)
{
continue;
}
// We only need to keep one AttributeRouteLinkGenerationEntry per route template
// so in case two entries have the same name and the same template we only keep
// the first entry.
AttributeRouteLinkGenerationEntry namedEntry = null;
if (namedEntries.TryGetValue(entry.Name, out namedEntry) &&
!namedEntry.TemplateText.Equals(entry.TemplateText, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(
Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.Name),
"linkGenerationEntries");
}
else if (namedEntry == null)
{
namedEntries.Add(entry.Name, entry);
}
}
_namedEntries = namedEntries;
// The decision tree will take care of ordering for these entries.
_linkGenerationTree = new LinkGenerationDecisionTree(linkGenerationEntries.ToArray());
}
/// <summary>
/// Gets the version of this route. This corresponds to the value of
/// <see cref="ActionDescriptorsCollection.Version"/> when this route was created.
/// </summary>
public int Version { get; }
/// <inheritdoc />
public async Task RouteAsync([NotNull] RouteContext context)
{
using (_logger.BeginScope("AttributeRoute.RouteAsync"))
{
foreach (var route in _matchingRoutes)
{
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(route);
try
{
context.RouteData = newRouteData;
await route.RouteAsync(context);
}
finally
{
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
if (context.IsHandled)
{
break;
}
}
}
if (_logger.IsEnabled(LogLevel.Verbose))
{
_logger.WriteValues(new AttributeRouteRouteAsyncValues()
{
MatchingRoutes = _matchingRoutes,
Handled = context.IsHandled
});
}
}
/// <inheritdoc />
public string GetVirtualPath([NotNull] VirtualPathContext context)
{
// If it's a named route we will try to generate a link directly and
// if we can't, we will not try to generate it using an unnamed route.
if (context.RouteName != null)
{
return GetVirtualPathForNamedRoute(context);
}
// The decision tree will give us back all entries that match the provided route data in the correct
// order. We just need to iterate them and use the first one that can generate a link.
var matches = _linkGenerationTree.GetMatches(context);
foreach (var entry in matches)
{
var path = GenerateLink(context, entry);
if (path != null)
{
context.IsBound = true;
return path;
}
}
return null;
}
private string GetVirtualPathForNamedRoute(VirtualPathContext context)
{
AttributeRouteLinkGenerationEntry entry;
if (_namedEntries.TryGetValue(context.RouteName, out entry))
{
var path = GenerateLink(context, entry);
if (path != null)
{
context.IsBound = true;
return path;
}
}
return null;
}
private string GenerateLink(VirtualPathContext context, AttributeRouteLinkGenerationEntry 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,
_constraintLogger);
if (!matched)
{
// A constraint 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))
{
// If the required value is an 'empty' route value, then ignore ambient values.
// This handles a case where we're generating a link to an action like:
// { area = "", controller = "Home", action = "Index" }
//
// and the ambient values has a value for area.
if (value != null)
{
context.AmbientValues.TryGetValue(key, out providedValue);
}
}
return TemplateBinder.RoutePartsEqual(providedValue, value);
}
}
}

View File

@ -4,7 +4,10 @@
#if ASPNET50
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
@ -29,12 +32,14 @@ namespace Microsoft.AspNet.Mvc.Routing
"and can occur only at the start of the parameter." + Environment.NewLine +
"Parameter name: routeTemplate";
var router = CreateRouter();
var handler = CreateRouter();
var services = CreateServices(action);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
var ex = Assert.Throws<InvalidOperationException>(() =>
{
AttributeRouting.CreateAttributeMegaRoute(handler, services);
});
Assert.Equal(expectedMessage, ex.Message);
}
@ -53,12 +58,14 @@ namespace Microsoft.AspNet.Mvc.Routing
"Error: The attribute route '{foo}/{action}' cannot contain a parameter named '{foo}'. " +
"Use '[foo]' in the route template to insert the value 'bleh'.";
var router = CreateRouter();
var handler = CreateRouter();
var services = CreateServices(action);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
var ex = Assert.Throws<InvalidOperationException>(() =>
{
AttributeRouting.CreateAttributeMegaRoute(handler, services);
});
Assert.Equal(expectedMessage, ex.Message);
}
@ -84,18 +91,20 @@ namespace Microsoft.AspNet.Mvc.Routing
"Error: The attribute route 'cool/{action}' cannot contain a parameter named '{action}'. " +
"Use '[action]' in the route template to insert the value 'hey'.";
var router = CreateRouter();
var handler = CreateRouter();
var services = CreateServices(action1, action2);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
var ex = Assert.Throws<InvalidOperationException>(() =>
{
AttributeRouting.CreateAttributeMegaRoute(handler, services);
});
Assert.Equal(expectedMessage, ex.Message);
}
[Fact]
public void AttributeRouting_WithReflectedActionDescriptor()
public void AttributeRouting_WithControllerActionDescriptor()
{
// Arrange
var controllerType = typeof(HomeController);
@ -121,12 +130,14 @@ namespace Microsoft.AspNet.Mvc.Routing
"Error: The attribute route '{controller}/{action}' cannot contain a parameter named '{controller}'. " +
"Use '[controller]' in the route template to insert the value 'Home'.";
var router = CreateRouter();
var handler = CreateRouter();
var services = CreateServices(action);
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(
() => { AttributeRouting.CreateAttributeMegaRoute(router, services); });
var ex = Assert.Throws<InvalidOperationException>(() =>
{
AttributeRouting.CreateAttributeMegaRoute(handler, services);
});
Assert.Equal(expectedMessage, ex.Message);
}
@ -172,6 +183,10 @@ namespace Microsoft.AspNet.Mvc.Routing
.Setup(s => s.GetService(typeof(IInlineConstraintResolver)))
.Returns(new DefaultInlineConstraintResolver(routeOptions.Object));
services
.Setup(s => s.GetService(typeof(ILoggerFactory)))
.Returns(NullLoggerFactory.Instance);
return services.Object;
}

File diff suppressed because it is too large Load Diff