diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
index 7ed95e37b1..e5cf507313 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRoute.cs
@@ -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
{
- ///
- /// An implementation for attribute routing.
- ///
public class AttributeRoute : IRouter
{
- private readonly IRouter _next;
- private readonly LinkGenerationDecisionTree _linkGenerationTree;
- private readonly TemplateRoute[] _matchingRoutes;
- private readonly IDictionary _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;
- ///
- /// Creates a new .
- ///
- /// The next router. Invoked when a route entry matches.
- /// The set of route entries.
public AttributeRoute(
- [NotNull] IRouter next,
- [NotNull] IEnumerable matchingEntries,
- [NotNull] IEnumerable 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();
+ _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(
- 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();
- _constraintLogger = factory.Create(typeof(RouteConstraintMatcher).FullName);
+ // Force creation of the route to report issues on startup.
+ GetInnerRoute();
}
///
- public async Task RouteAsync([NotNull] RouteContext context)
+ public string GetVirtualPath(VirtualPathContext context)
{
- using (_logger.BeginScope("AttributeRoute.RouteAsync"))
+ var route = GetInnerRoute();
+ return route.GetVirtualPath(context);
+ }
+
+ ///
+ 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();
+ 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,
});
}
- }
- ///
- 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();
+ 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(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 GroupRouteInfosByGroupId(List routeInfos)
+ {
+ var routeInfosByGroupId = new Dictionary(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 GetRouteInfos(
+ IInlineConstraintResolver constraintResolver,
+ IReadOnlyList actions)
{
- AttributeRouteLinkGenerationEntry entry;
- if (_namedEntries.TryGetValue(context.RouteName, out entry))
+ var routeInfos = new List();
+ var errors = new List();
+
+ // 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(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 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(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(
- 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 Constraints { get; set; }
+
+ public IReadOnlyDictionary 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; }
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
index 4c3b5a9626..2fa3f1aec4 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteLinkGenerationEntry.cs
@@ -8,8 +8,8 @@ using Microsoft.AspNet.Routing.Template;
namespace Microsoft.AspNet.Mvc.Routing
{
///
- /// Used to build an . Represents an individual URL-generating route that will be
- /// aggregated into the .
+ /// Used to build an . Represents an individual URL-generating route that will be
+ /// aggregated into the .
///
public class AttributeRouteLinkGenerationEntry
{
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs
index 505dfd2e0c..5629183c58 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouteMatchingEntry.cs
@@ -6,8 +6,8 @@ using Microsoft.AspNet.Routing.Template;
namespace Microsoft.AspNet.Mvc.Routing
{
///
- /// Used to build an . Represents an individual URL-matching route that will be
- /// aggregated into the .
+ /// Used to build an . Represents an individual URL-matching route that will be
+ /// aggregated into the .
///
public class AttributeRouteMatchingEntry
{
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
index b7b06e570b..659badb169 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/AttributeRouting.cs
@@ -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
/// The application services.
/// An attribute route.
public static IRouter CreateAttributeMegaRoute([NotNull] IRouter target, [NotNull] IServiceProvider services)
- {
- var actions = GetActionDescriptors(services);
-
- var inlineConstraintResolver = services.GetRequiredService();
- 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();
- 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();
- foreach (var routeInfo in distinctRouteInfosByGroup)
- {
- matchingEntries.Add(new AttributeRouteMatchingEntry()
- {
- Order = routeInfo.Order,
- Precedence = routeInfo.Precedence,
- Route = new TemplateRoute(
- target,
- routeInfo.RouteTemplate,
- defaults: new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- { RouteGroupKey, routeInfo.RouteGroup },
- },
- constraints: null,
- dataTokens: null,
- inlineConstraintResolver: inlineConstraintResolver),
- });
- }
-
- return new AttributeRoute(
- target,
- matchingEntries,
- generationEntries,
- services.GetRequiredService());
- }
-
- private static IReadOnlyList GetActionDescriptors(IServiceProvider services)
{
var actionDescriptorProvider = services.GetRequiredService();
+ var inlineConstraintResolver = services.GetRequiredService();
+ var loggerFactory = services.GetRequiredService();
- var actionDescriptorsCollection = actionDescriptorProvider.ActionDescriptors;
- return actionDescriptorsCollection.Items;
- }
-
- private static IEnumerable GroupRouteInfosByGroupId(List routeInfos)
- {
- var routeInfosByGroupId = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- foreach (var routeInfo in routeInfos)
- {
- if (!routeInfosByGroupId.ContainsKey(routeInfo.RouteGroup))
- {
- routeInfosByGroupId.Add(routeInfo.RouteGroup, routeInfo);
- }
- }
-
- return routeInfosByGroupId.Values;
- }
-
- private static List GetRouteInfos(
- IInlineConstraintResolver constraintResolver,
- IReadOnlyList actions)
- {
- var routeInfos = new List();
- var errors = new List();
-
- // 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(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 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 Constraints { get; set; }
-
- public IReadOnlyDictionary 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);
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs b/src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs
new file mode 100644
index 0000000000..676bbf67a8
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs
@@ -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
+{
+ ///
+ /// An implementation for attribute routing.
+ ///
+ public class InnerAttributeRoute : IRouter
+ {
+ private readonly IRouter _next;
+ private readonly LinkGenerationDecisionTree _linkGenerationTree;
+ private readonly TemplateRoute[] _matchingRoutes;
+ private readonly IDictionary _namedEntries;
+
+ private ILogger _logger;
+ private ILogger _constraintLogger;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The next router. Invoked when a route entry matches.
+ /// The set of route entries.
+ public InnerAttributeRoute(
+ [NotNull] IRouter next,
+ [NotNull] IEnumerable matchingEntries,
+ [NotNull] IEnumerable 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(
+ 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());
+ }
+
+ ///
+ /// Gets the version of this route. This corresponds to the value of
+ /// when this route was created.
+ ///
+ public int Version { get; }
+
+ ///
+ 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
+ });
+ }
+ }
+
+ ///
+ 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(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(
+ 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);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs
index 5f44dcfa77..ec0e7cfd22 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRouteTest.cs
@@ -1,16 +1,14 @@
// 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.
+#if ASPNET50
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
-using Microsoft.AspNet.Mvc.Logging;
+using Microsoft.AspNet.Http.Core;
using Microsoft.AspNet.Routing;
-using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.Logging;
-using Microsoft.Framework.OptionsModel;
using Moq;
using Xunit;
@@ -18,1729 +16,95 @@ namespace Microsoft.AspNet.Mvc.Routing
{
public class AttributeRouteTest
{
- [Theory]
- [InlineData("template/5", "template/{parameter:int}")]
- [InlineData("template/5", "template/{parameter}")]
- [InlineData("template/5", "template/{*parameter:int}")]
- [InlineData("template/5", "template/{*parameter}")]
- [InlineData("template/{parameter:int}", "template/{parameter}")]
- [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
- [InlineData("template/{parameter:int}", "template/{*parameter}")]
- [InlineData("template/{parameter}", "template/{*parameter:int}")]
- [InlineData("template/{parameter}", "template/{*parameter}")]
- [InlineData("template/{*parameter:int}", "template/{*parameter}")]
- public async Task AttributeRoute_RouteAsync_RespectsPrecedence(
- string firstTemplate,
- string secondTemplate)
+ // This test verifies that AttributeRoute can respond to changes in the AD collection. It does this
+ // by running a successful request, then removing that action and verifying the next route isn't
+ // successful.
+ [Fact]
+ public async Task AttributeRoute_UsesUpdatedActionDescriptors()
{
// Arrange
- var expectedRouteGroup = string.Format("{0}&&{1}", 0, firstTemplate);
+ var handler = new Mock(MockBehavior.Strict);
+ handler
+ .Setup(h => h.RouteAsync(It.IsAny()))
+ .Callback(c => c.IsHandled = true)
+ .Returns(Task.FromResult(true))
+ .Verifiable();
- // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
- var numberOfCalls = 0;
- Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
-
- var next = new Mock();
- next.Setup(r => r.RouteAsync(It.IsAny()))
- .Callback(callBack)
- .Returns(Task.FromResult(true))
- .Verifiable();
-
- var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 0);
- var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0);
-
- // We setup the route entries in reverse order of precedence to ensure that when we
- // try to route the request, the route with a higher precedence gets tried first.
- var matchingRoutes = new[] { secondRoute, firstRoute };
-
- var linkGenerationEntries = Enumerable.Empty();
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateRouteContext("/template/5");
-
- // Act
- await route.RouteAsync(context);
-
- // Assert
- Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
- }
-
- [Theory]
- [InlineData("template/5", "template/{parameter:int}")]
- [InlineData("template/5", "template/{parameter}")]
- [InlineData("template/5", "template/{*parameter:int}")]
- [InlineData("template/5", "template/{*parameter}")]
- [InlineData("template/{parameter:int}", "template/{parameter}")]
- [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
- [InlineData("template/{parameter:int}", "template/{*parameter}")]
- [InlineData("template/{parameter}", "template/{*parameter:int}")]
- [InlineData("template/{parameter}", "template/{*parameter}")]
- [InlineData("template/{*parameter:int}", "template/{*parameter}")]
- public async Task AttributeRoute_RouteAsync_RespectsOrderOverPrecedence(
- string firstTemplate,
- string secondTemplate)
- {
- // Arrange
- var expectedRouteGroup = string.Format("{0}&&{1}", 0, secondTemplate);
-
- // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
- var numberOfCalls = 0;
- Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
-
- var next = new Mock();
- next.Setup(r => r.RouteAsync(It.IsAny()))
- .Callback(callBack)
- .Returns(Task.FromResult(true))
- .Verifiable();
-
- var firstRoute = CreateMatchingEntry(next.Object, firstTemplate, order: 1);
- var secondRoute = CreateMatchingEntry(next.Object, secondTemplate, order: 0);
-
- // We setup the route entries with a lower relative order and higher relative precedence
- // first to ensure that when we try to route the request, the route with the higher
- // relative order gets tried first.
- var matchingRoutes = new[] { firstRoute, secondRoute };
-
- var linkGenerationEntries = Enumerable.Empty();
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateRouteContext("/template/5");
-
- // Act
- await route.RouteAsync(context);
-
- // Assert
- Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
- }
-
- [Theory]
- [InlineData("template/5")]
- [InlineData("template/{parameter:int}")]
- [InlineData("template/{parameter}")]
- [InlineData("template/{*parameter:int}")]
- [InlineData("template/{*parameter}")]
- public async Task AttributeRoute_RouteAsync_RespectsOrder(string template)
- {
- // Arrange
- var expectedRouteGroup = string.Format("{0}&&{1}", 0, template);
-
- // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
- var numberOfCalls = 0;
- Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
-
- var next = new Mock();
- next.Setup(r => r.RouteAsync(It.IsAny()))
- .Callback(callBack)
- .Returns(Task.FromResult(true))
- .Verifiable();
-
- var firstRoute = CreateMatchingEntry(next.Object, template, order: 1);
- var secondRoute = CreateMatchingEntry(next.Object, template, order: 0);
-
- // We setup the route entries with a lower relative order first to ensure that when
- // we try to route the request, the route with the higher relative order gets tried first.
- var matchingRoutes = new[] { firstRoute, secondRoute };
-
- var linkGenerationEntries = Enumerable.Empty();
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateRouteContext("/template/5");
-
- // Act
- await route.RouteAsync(context);
-
- // Assert
- Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
- }
-
- [Theory]
- [InlineData("template/{first:int}", "template/{second:int}")]
- [InlineData("template/{first}", "template/{second}")]
- [InlineData("template/{*first:int}", "template/{*second:int}")]
- [InlineData("template/{*first}", "template/{*second}")]
- public async Task AttributeRoute_RouteAsync_EnsuresStableOrdering(string first, string second)
- {
- // Arrange
- var expectedRouteGroup = string.Format("{0}&&{1}", 0, first);
-
- // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
- var numberOfCalls = 0;
- Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
-
- var next = new Mock();
- next.Setup(r => r.RouteAsync(It.IsAny()))
- .Callback(callBack)
- .Returns(Task.FromResult(true))
- .Verifiable();
-
- var secondRouter = new Mock(MockBehavior.Strict);
-
- var firstRoute = CreateMatchingEntry(next.Object, first, order: 0);
- var secondRoute = CreateMatchingEntry(next.Object, second, order: 0);
-
- // We setup the route entries with a lower relative template order first to ensure that when
- // we try to route the request, the route with the higher template order gets tried first.
- var matchingRoutes = new[] { secondRoute, firstRoute };
-
- var linkGenerationEntries = Enumerable.Empty();
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateRouteContext("/template/5");
-
- // Act
- await route.RouteAsync(context);
-
- // Assert
- Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
- }
-
- [Theory]
- [InlineData("template/{parameter:int}", "/template/5", true)]
- [InlineData("template/{parameter:int?}", "/template/5", true)]
- [InlineData("template/{parameter:int?}", "/template", true)]
- [InlineData("template/{parameter:int?}", "/template/qwer", false)]
- public async Task AttributeRoute_WithOptionalInlineConstraint(string template, string request, bool expectedResult)
- {
- // Arrange
- var expectedRouteGroup = string.Format("{0}&&{1}", 0, template);
-
- // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
- var numberOfCalls = 0;
- Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
-
- var next = new Mock();
- next.Setup(r => r.RouteAsync(It.IsAny()))
- .Callback(callBack)
- .Returns(Task.FromResult(true))
- .Verifiable();
-
- var firstRoute = CreateMatchingEntry(next.Object, template, order: 0);
-
- // We setup the route entries in reverse order of precedence to ensure that when we
- // try to route the request, the route with a higher precedence gets tried first.
- var matchingRoutes = new[] { firstRoute };
-
- var linkGenerationEntries = Enumerable.Empty();
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateRouteContext(request);
-
- // Act
- await route.RouteAsync(context);
-
- // Assert
- if (expectedResult)
+ var actionDescriptors = new List()
{
- Assert.True(context.IsHandled);
- Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]);
- }
- else
- {
- Assert.False(context.IsHandled);
- }
- }
-
- [Theory]
- [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)]
- [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)]
- [InlineData("moo/{p1?}", "/moo", null, null, null)]
- [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)]
- [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)]
- [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)]
- [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)]
- [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)]
- [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)]
- [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)]
- [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)]
- [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)]
- [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
- [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)]
- [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
- [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")]
- [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")]
- [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")]
- public async Task AttributeRoute_WithOptionalCompositeParameter_Valid(
- string template,
- string request,
- string p1,
- string p2,
- string p3)
- {
- // Arrange
- var expectedRouteGroup = string.Format("{0}&&{1}", 0, template);
-
- // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
- var numberOfCalls = 0;
- Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
-
- var next = new Mock();
- next.Setup(r => r.RouteAsync(It.IsAny()))
- .Callback(callBack)
- .Returns(Task.FromResult(true))
- .Verifiable();
-
- var firstRoute = CreateMatchingEntry(next.Object, template, order: 0);
-
- // We setup the route entries in reverse order of precedence to ensure that when we
- // try to route the request, the route with a higher precedence gets tried first.
- var matchingRoutes = new[] { firstRoute };
- var linkGenerationEntries = Enumerable.Empty();
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
- var context = CreateRouteContext(request);
-
- // Act
- await route.RouteAsync(context);
-
- // Assert
- Assert.True(context.IsHandled);
- if (p1 != null)
- {
- Assert.Equal(p1, context.RouteData.Values["p1"]);
- }
- if (p2 != null)
- {
- Assert.Equal(p2, context.RouteData.Values["p2"]);
- }
- if (p3 != null)
- {
- Assert.Equal(p3, context.RouteData.Values["p3"]);
- }
- }
-
- [Theory]
- [InlineData("moo/{p1}.{p2?}", "/moo/foo.")]
- [InlineData("moo/{p1}.{p2?}", "/moo/.")]
- [InlineData("moo/{p1}.{p2}", "/foo.")]
- [InlineData("moo/{p1}.{p2}", "/foo")]
- [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")]
- [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")]
- [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")]
- [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")]
- [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")]
- [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")]
- [InlineData("moo/.{p2?}", "/moo/.")]
- [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")]
- public async Task AttributeRoute_WithOptionalCompositeParameter_Invalid(
- string template,
- string request)
- {
- // Arrange
- var expectedRouteGroup = string.Format("{0}&&{1}", 0, template);
-
- // We need to force the creation of a closure in order to avoid an issue with Moq and Roslyn.
- var numberOfCalls = 0;
- Action callBack = ctx => { ctx.IsHandled = true; numberOfCalls++; };
-
- var next = new Mock();
- next.Setup(r => r.RouteAsync(It.IsAny()))
- .Callback(callBack)
- .Returns(Task.FromResult(true))
- .Verifiable();
-
- var firstRoute = CreateMatchingEntry(next.Object, template, order: 0);
-
- // We setup the route entries in reverse order of precedence to ensure that when we
- // try to route the request, the route with a higher precedence gets tried first.
- var matchingRoutes = new[] { firstRoute };
- var linkGenerationEntries = Enumerable.Empty();
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
- var context = CreateRouteContext(request);
-
- // Act
- await route.RouteAsync(context);
-
- // Assert
- Assert.False(context.IsHandled);
- }
-
- [Theory]
- [InlineData("template/5", "template/{parameter:int}")]
- [InlineData("template/5", "template/{parameter}")]
- [InlineData("template/5", "template/{*parameter:int}")]
- [InlineData("template/5", "template/{*parameter}")]
- [InlineData("template/{parameter:int}", "template/{parameter}")]
- [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
- [InlineData("template/{parameter:int}", "template/{*parameter}")]
- [InlineData("template/{parameter}", "template/{*parameter:int}")]
- [InlineData("template/{parameter}", "template/{*parameter}")]
- [InlineData("template/{*parameter:int}", "template/{*parameter}")]
- public void AttributeRoute_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate)
- {
- // Arrange
- var expectedGroup = CreateRouteGroup(0, firstTemplate);
-
- string selectedGroup = null;
-
- var next = new Mock();
- next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx =>
- {
- selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
- ctx.IsBound = true;
- })
- .Returns((string)null);
-
- var matchingRoutes = Enumerable.Empty();
-
- var firstEntry = CreateGenerationEntry(firstTemplate, requiredValues: null);
- var secondEntry = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
-
- // We setup the route entries in reverse order of precedence to ensure that when we
- // try to generate a link, the route with a higher precedence gets tried first.
- var linkGenerationEntries = new[] { secondEntry, firstEntry };
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 });
-
- // Act
- string result = route.GetVirtualPath(context);
-
- // Assert
- Assert.NotNull(result);
- Assert.Equal("template/5", result);
- Assert.Equal(expectedGroup, selectedGroup);
- }
-
- [Theory]
- [InlineData("template/{parameter:int}", "template/5", 5)]
- [InlineData("template/{parameter:int?}", "template/5", 5)]
- [InlineData("template/{parameter:int?}", "template", null)]
- [InlineData("template/{parameter:int?}", null, "asdf")]
- [InlineData("template/{parameter:alpha?}", "template/asdf", "asdf")]
- [InlineData("template/{parameter:alpha?}", "template", null)]
- [InlineData("template/{parameter:int:range(1,20)?}", "template", null)]
- [InlineData("template/{parameter:int:range(1,20)?}", "template/5", 5)]
- [InlineData("template/{parameter:int:range(1,20)?}", null, 21)]
- public void AttributeRoute_GenerateLink_OptionalInlineParameter(string template, string expectedResult, object parameter)
- {
- // Arrange
- var expectedGroup = CreateRouteGroup(0, template);
-
- string selectedGroup = null;
-
- var next = new Mock();
- next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx =>
- {
- selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
- ctx.IsBound = true;
- })
- .Returns((string)null);
-
- var matchingRoutes = Enumerable.Empty();
-
- var entry = CreateGenerationEntry(template, requiredValues: null);
-
- var linkGenerationEntries = new[] { entry };
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
- VirtualPathContext context;
-
- if (parameter != null)
- {
- context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = parameter });
- }
- else
- {
- context = CreateVirtualPathContext(values: null, ambientValues: null);
- }
-
- // Act
- string result = route.GetVirtualPath(context);
-
- // Assert
- Assert.Equal(expectedResult, result);
- }
-
- [Theory]
- [InlineData("template/5", "template/{parameter:int}")]
- [InlineData("template/5", "template/{parameter}")]
- [InlineData("template/5", "template/{*parameter:int}")]
- [InlineData("template/5", "template/{*parameter}")]
- [InlineData("template/{parameter:int}", "template/{parameter}")]
- [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
- [InlineData("template/{parameter:int}", "template/{*parameter}")]
- [InlineData("template/{parameter}", "template/{*parameter:int}")]
- [InlineData("template/{parameter}", "template/{*parameter}")]
- [InlineData("template/{*parameter:int}", "template/{*parameter}")]
- public void AttributeRoute_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate)
- {
- // Arrange
- var selectedGroup = CreateRouteGroup(0, secondTemplate);
-
- string firstRouteGroupSelected = null;
- var next = new Mock();
- next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx =>
- {
- firstRouteGroupSelected = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
- ctx.IsBound = true;
- })
- .Returns((string)null);
-
- var matchingRoutes = Enumerable.Empty();
-
- var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1);
- var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
-
- // We setup the route entries with a lower relative order and higher relative precedence
- // first to ensure that when we try to generate a link, the route with the higher
- // relative order gets tried first.
- var linkGenerationEntries = new[] { firstRoute, secondRoute };
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 });
-
- // Act
- string result = route.GetVirtualPath(context);
-
- // Assert
- Assert.NotNull(result);
- Assert.Equal("template/5", result);
- Assert.Equal(selectedGroup, firstRouteGroupSelected);
- }
-
- [Theory]
- [InlineData("template/5", "template/5")]
- [InlineData("template/{first:int}", "template/{second:int}")]
- [InlineData("template/{first}", "template/{second}")]
- [InlineData("template/{*first:int}", "template/{*second:int}")]
- [InlineData("template/{*first}", "template/{*second}")]
- public void AttributeRoute_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate)
- {
- // Arrange
- var expectedGroup = CreateRouteGroup(0, secondTemplate);
-
- var next = new Mock();
- string selectedGroup = null;
- next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx =>
- {
- selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
- ctx.IsBound = true;
- })
- .Returns((string)null);
-
- var matchingRoutes = Enumerable.Empty();
-
- var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 1);
- var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
-
- // We setup the route entries with a lower relative order first to ensure that when
- // we try to generate a link, the route with the higher relative order gets tried first.
- var linkGenerationEntries = new[] { firstRoute, secondRoute };
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 });
-
- // Act
- string result = route.GetVirtualPath(context);
-
- // Assert
- Assert.NotNull(result);
- Assert.Equal("template/5", result);
- Assert.Equal(expectedGroup, selectedGroup);
- }
-
- [Theory]
- [InlineData("first/5", "second/5")]
- [InlineData("first/{first:int}", "second/{second:int}")]
- [InlineData("first/{first}", "second/{second}")]
- [InlineData("first/{*first:int}", "second/{*second:int}")]
- [InlineData("first/{*first}", "second/{*second}")]
- public void AttributeRoute_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate)
- {
- // Arrange
- var expectedGroup = CreateRouteGroup(0, firstTemplate);
-
- var next = new Mock();
- string selectedGroup = null;
- next.Setup(n => n.GetVirtualPath(It.IsAny())).Callback(ctx =>
- {
- selectedGroup = (string)ctx.ProvidedValues[AttributeRouting.RouteGroupKey];
- ctx.IsBound = true;
- })
- .Returns((string)null);
-
- var matchingRoutes = Enumerable.Empty();
-
- var firstRoute = CreateGenerationEntry(firstTemplate, requiredValues: null, order: 0);
- var secondRoute = CreateGenerationEntry(secondTemplate, requiredValues: null, order: 0);
-
- // We setup the route entries with a lower relative template order first to ensure that when
- // we try to generate a link, the route with the higher template order gets tried first.
- var linkGenerationEntries = new[] { secondRoute, firstRoute };
-
- var route = new AttributeRoute(next.Object, matchingRoutes, linkGenerationEntries, NullLoggerFactory.Instance);
-
- var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 });
-
- // Act
- string result = route.GetVirtualPath(context);
-
- // Assert
- Assert.NotNull(result);
- Assert.Equal("first/5", result);
- Assert.Equal(expectedGroup, selectedGroup);
- }
-
- public static IEnumerable