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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Internal.Routing;
using Microsoft.AspNet.Mvc.Logging;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template; using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
namespace Microsoft.AspNet.Mvc.Routing namespace Microsoft.AspNet.Mvc.Routing
{ {
/// <summary>
/// An <see cref="IRouter"/> implementation for attribute routing.
/// </summary>
public class AttributeRoute : IRouter public class AttributeRoute : IRouter
{ {
private readonly IRouter _next; private readonly IRouter _target;
private readonly LinkGenerationDecisionTree _linkGenerationTree; private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider;
private readonly TemplateRoute[] _matchingRoutes; private readonly IInlineConstraintResolver _constraintResolver;
private readonly IDictionary<string, AttributeRouteLinkGenerationEntry> _namedEntries;
private ILogger _logger; // These loggers are used by the inner route, keep them around to avoid re-creating.
private ILogger _constraintLogger; 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( public AttributeRoute(
[NotNull] IRouter next, [NotNull] IRouter target,
[NotNull] IEnumerable<AttributeRouteMatchingEntry> matchingEntries, [NotNull] IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider,
[NotNull] IEnumerable<AttributeRouteLinkGenerationEntry> linkGenerationEntries, [NotNull] IInlineConstraintResolver constraintResolver,
[NotNull] ILoggerFactory factory) [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 _routeLogger = loggerFactory.Create<InnerAttributeRoute>();
// a stable routing and link generation order for templates with same order and precedence. _constraintLogger = loggerFactory.Create(typeof(RouteConstraintMatcher).FullName);
// 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 // Force creation of the route to report issues on startup.
.OrderBy(o => o.Order) GetInnerRoute();
.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);
} }
/// <inheritdoc /> /// <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) _inner = BuildRoute(actions);
{
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)) 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, Binder = new TemplateBinder(routeInfo.ParsedTemplate, routeInfo.Defaults),
Handled = context.IsHandled 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 /> // We're creating one AttributeRouteMatchingEntry per group, so we need to identify the distinct set of
public string GetVirtualPath([NotNull] VirtualPathContext context) // 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.
// If it's a named route we will try to generate a link directly and var distinctRouteInfosByGroup = GroupRouteInfosByGroupId(routeInfos);
// if we can't, we will not try to generate it using an unnamed route. var matchingEntries = new List<AttributeRouteMatchingEntry>();
if (context.RouteName != null) 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 return new InnerAttributeRoute(
// order. We just need to iterate them and use the first one that can generate a link. _target,
var matches = _linkGenerationTree.GetMatches(context); 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 (!routeInfosByGroupId.ContainsKey(routeInfo.RouteGroup))
if (path != null)
{ {
context.IsBound = true; routeInfosByGroupId.Add(routeInfo.RouteGroup, routeInfo);
return path;
} }
} }
return null; return routeInfosByGroupId.Values;
} }
private string GetVirtualPathForNamedRoute(VirtualPathContext context) private static List<RouteInfo> GetRouteInfos(
IInlineConstraintResolver constraintResolver,
IReadOnlyList<ActionDescriptor> actions)
{ {
AttributeRouteLinkGenerationEntry entry; var routeInfos = new List<RouteInfo>();
if (_namedEntries.TryGetValue(context.RouteName, out entry)) 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); var routeInfo = GetRouteInfo(constraintResolver, templateCache, action);
if (path != null) if (routeInfo.ErrorMessage == null)
{ {
context.IsBound = true; routeInfos.Add(routeInfo);
return path; }
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 var constraint = action.RouteConstraints
// these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't .Where(c => c.RouteKey == AttributeRouting.RouteGroupKey)
// want to pass these to the link generation code, or else they will end up as query parameters. .FirstOrDefault();
// if (constraint == null ||
// So, we need to exclude from here any values that are 'required link values', but aren't constraint.KeyHandling != RouteKeyHandling.RequireKey ||
// parameters in the template. constraint.RouteValue == null)
//
// 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)) // 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
var parameter = entry.Template.Parameters // routing system, or has customized their ADs in a way that we can no longer understand them.
.FirstOrDefault(p => string.Equals(p.Name, kvp.Key, StringComparison.OrdinalIgnoreCase)); //
// We just treat this case as an 'opt-out' of our attribute routing system.
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; return null;
} }
var matched = RouteConstraintMatcher.Match( var routeInfo = new RouteInfo()
entry.Constraints,
bindingResult.CombinedValues,
context.Context,
this,
RouteDirection.UrlGeneration,
_constraintLogger);
if (!matched)
{ {
// A constraint rejected this link. ActionDescriptor = action,
return null; RouteGroup = constraint.RouteValue,
} RouteTemplate = action.AttributeRouteInfo.Template,
// 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); try
if (path != null)
{ {
// If path is non-null then the target router short-circuited, we don't expect this RouteTemplate parsedTemplate;
// in typical MVC scenarios. if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate))
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); // 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 namespace Microsoft.AspNet.Mvc.Routing
{ {
/// <summary> /// <summary>
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual URL-generating route that will be /// Used to build an <see cref="InnerAttributeRoute"/>. Represents an individual URL-generating route that will be
/// aggregated into the <see cref="AttributeRoute"/>. /// aggregated into the <see cref="InnerAttributeRoute"/>.
/// </summary> /// </summary>
public class AttributeRouteLinkGenerationEntry public class AttributeRouteLinkGenerationEntry
{ {

View File

@ -6,8 +6,8 @@ using Microsoft.AspNet.Routing.Template;
namespace Microsoft.AspNet.Mvc.Routing namespace Microsoft.AspNet.Mvc.Routing
{ {
/// <summary> /// <summary>
/// Used to build an <see cref="AttributeRoute"/>. Represents an individual URL-matching route that will be /// Used to build an <see cref="InnerAttributeRoute"/>. Represents an individual URL-matching route that will be
/// aggregated into the <see cref="AttributeRoute"/>. /// aggregated into the <see cref="InnerAttributeRoute"/>.
/// </summary> /// </summary>
public class AttributeRouteMatchingEntry 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging; using Microsoft.Framework.Logging;
@ -25,247 +21,12 @@ namespace Microsoft.AspNet.Mvc.Routing
/// <param name="services">The application services.</param> /// <param name="services">The application services.</param>
/// <returns>An attribute route.</returns> /// <returns>An attribute route.</returns>
public static IRouter CreateAttributeMegaRoute([NotNull] IRouter target, [NotNull] IServiceProvider services) 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 actionDescriptorProvider = services.GetRequiredService<IActionDescriptorsCollectionProvider>();
var inlineConstraintResolver = services.GetRequiredService<IInlineConstraintResolver>();
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
var actionDescriptorsCollection = actionDescriptorProvider.ActionDescriptors; return new AttributeRoute(target, actionDescriptorProvider, inlineConstraintResolver, loggerFactory);
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; }
} }
} }
} }

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

File diff suppressed because it is too large Load Diff