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 NamedEntriesWithDifferentTemplates - { - get - { - var data = new TheoryData>(); - data.Add(new[] + new ActionDescriptor() { - CreateGenerationEntry("template", null, 0, "NamedEntry"), - CreateGenerationEntry("otherTemplate", null, 0, "NamedEntry"), - CreateGenerationEntry("anotherTemplate", null, 0, "NamedEntry") - }); - - // Default values for parameters are taken into account by comparing the templates. - data.Add(new[] + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Blog/{id}" + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "1"), + }, + }, + new ActionDescriptor() { - CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{parameter=1}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{parameter=2}", null, 0, "NamedEntry") - }); + AttributeRouteInfo = new AttributeRouteInfo() + { + Template = "api/Store/Buy/{id}" + }, + RouteConstraints = new List() + { + new RouteDataActionConstraint(AttributeRouting.RouteGroupKey, "2"), + }, + }, + }; - // Names for entries are compared ignoring casing. - data.Add(new[] - { - CreateGenerationEntry("template/{*parameter:int=0}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{*parameter:int=1}", null, 0, "NAMEDENTRY"), - CreateGenerationEntry("template/{*parameter:int=2}", null, 0, "namedentry") - }); - return data; - } - } - - [Theory] - [MemberData(nameof(AttributeRouteTest.NamedEntriesWithDifferentTemplates))] - public void AttributeRoute_CreateAttributeRoute_ThrowsIfDifferentEntriesHaveTheSameName( - IEnumerable namedEntries) - { - // Arrange - string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." + - Environment.NewLine + - "Parameter name: linkGenerationEntries"; - - var next = new Mock().Object; - - var matchingEntries = Enumerable.Empty(); - - // Act - var exception = Assert.Throws( - "linkGenerationEntries", - () => new AttributeRoute( - next, - matchingEntries, - namedEntries, - NullLoggerFactory.Instance)); - - Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.OrdinalIgnoreCase); - } - - public static IEnumerable NamedEntriesWithTheSameTemplate - { - get - { - var data = new TheoryData>(); - - data.Add(new[] - { - CreateGenerationEntry("template", null, 0, "NamedEntry"), - CreateGenerationEntry("template", null, 1, "NamedEntry"), - CreateGenerationEntry("template", null, 2, "NamedEntry") - }); - - // Templates are compared ignoring casing. - data.Add(new[] - { - CreateGenerationEntry("template", null, 0, "NamedEntry"), - CreateGenerationEntry("Template", null, 1, "NamedEntry"), - CreateGenerationEntry("TEMPLATE", null, 2, "NamedEntry") - }); - - data.Add(new[] - { - CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"), - CreateGenerationEntry("template/{parameter=0}", null, 1, "NamedEntry"), - CreateGenerationEntry("template/{parameter=0}", null, 2, "NamedEntry") - }); - - return data; - } - } - - [Theory] - [MemberData(nameof(AttributeRouteTest.NamedEntriesWithTheSameTemplate))] - public void AttributeRoute_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate( - IEnumerable namedEntries) - { - // Arrange - var expectedLink = namedEntries.First().Template.Parameters.Any() ? "template/5" : "template"; - - var expectedGroup = "0&" + namedEntries.First().TemplateText; - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }); - - var matchingEntries = Enumerable.Empty(); + var actionDescriptorsProvider = new Mock(MockBehavior.Strict); + actionDescriptorsProvider + .SetupGet(ad => ad.ActionDescriptors) + .Returns(new ActionDescriptorsCollection(actionDescriptors, version: 1)); var route = new AttributeRoute( - next.Object, - matchingEntries, - namedEntries, - NullLoggerFactory.Instance); - - var ambientValues = namedEntries.First().Template.Parameters.Any() ? new { parameter = 5 } : null; - - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedEntry"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(expectedGroup, selectedGroup); - Assert.Equal(expectedLink, result); - } - - [Fact] - public void AttributeRoute_GenerateLink_WithName() - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }); - - var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute"); - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance); - - var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal("1&named", selectedGroup); - Assert.Equal("named", result); - } - - [Fact] - public void AttributeRoute_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName() - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }); - - var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute"); - - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance); - - var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.Null(result); - } - - [Theory] - [InlineData("template/{parameter:int}", null)] - [InlineData("template/{parameter:int}", "NaN")] - [InlineData("template/{parameter}", null)] - [InlineData("template/{*parameter:int}", null)] - [InlineData("template/{*parameter:int}", "NaN")] - public void AttributeRoute_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value) - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }); - - var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute"); - - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance); - - var ambientValues = value == null ? null : new { parameter = value }; - - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.Null(result); - } - - [Theory] - [InlineData("template/{parameter:int}", "5")] - [InlineData("template/{parameter}", "5")] - [InlineData("template/{*parameter:int}", "5")] - [InlineData("template/{*parameter}", "5")] - public void AttributeRoute_GeneratesLink_IfValuesMatchNamedEntry(string template, string value) - { - // Arrange - string selectedGroup = null; - var next = new Mock(); - next.Setup(s => s.GetVirtualPath(It.IsAny())) - .Callback(vpc => - { - vpc.IsBound = true; - selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; - }); - - var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute"); - - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); - - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; - - var matchingEntries = Enumerable.Empty(); - - var route = new AttributeRoute(next.Object, matchingEntries, linkGenerationEntries, NullLoggerFactory.Instance); - - var ambientValues = value == null ? null : new { parameter = value }; - - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(string.Format("1&{0}", template), selectedGroup); - Assert.Equal("template/5", result); - } - - [Fact] - public async void AttributeRoute_RouteAsyncHandled_LogsCorrectValues() - { - // Arrange - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink); - - var entry = CreateMatchingEntry("api/Store"); - var route = CreateRoutingAttributeRoute(loggerFactory, entry); - - var context = CreateRouteContext("/api/Store"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal(1, sink.Scopes.Count); - var scope = sink.Scopes[0]; - Assert.Equal(typeof(AttributeRoute).FullName, scope.LoggerName); - Assert.Equal("AttributeRoute.RouteAsync", scope.Scope); - - Assert.Equal(1, sink.Writes.Count); - - var write = sink.Writes[0]; - Assert.Equal(typeof(AttributeRoute).FullName, write.LoggerName); - Assert.Equal("AttributeRoute.RouteAsync", write.Scope); - var values = Assert.IsType(write.State); - Assert.Equal("AttributeRoute.RouteAsync", values.Name); - Assert.True(values.Handled); - } - - [Fact] - public async void AttributeRoute_RouteAsyncNotHandled_LogsCorrectValues() - { - // Arrange - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink); - - var entry = CreateMatchingEntry("api/Store"); - var route = CreateRoutingAttributeRoute(loggerFactory, entry); - - var context = CreateRouteContext("/"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Equal(1, sink.Scopes.Count); - var scope = sink.Scopes[0]; - Assert.Equal(typeof(AttributeRoute).FullName, scope.LoggerName); - Assert.Equal("AttributeRoute.RouteAsync", scope.Scope); - - Assert.Equal(1, sink.Writes.Count); - - var write = sink.Writes[0]; - Assert.Equal(typeof(AttributeRoute).FullName, write.LoggerName); - Assert.Equal("AttributeRoute.RouteAsync", write.Scope); - var values = Assert.IsType(write.State); - Assert.Equal("AttributeRoute.RouteAsync", values.Name); - Assert.False(values.Handled); - } - - [Fact] - public void AttributeRoute_GenerateLink_NoRequiredValues() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_NoMatch() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Details", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Null(path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithAmbientValues() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithParameters() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store/Index", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithMoreParameters() - { - // Arrange - var entry = CreateGenerationEntry( - "api/{area}/dosomething/{controller}/{action}", - new { action = "Index", controller = "Store", area = "AwesomeCo" }); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "area", "AwesomeCo" }, - { "controller", "Store" }, - { "action", "Index" }, - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); - - var context = CreateVirtualPathContext( - new { action = "Index", controller = "Store" }, - new { area = "AwesomeCo" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/AwesomeCo/dosomething/Store/Index", path); - Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithDefault() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action=Index}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithConstraint() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "action", "Index" }, - { "id", 5 }, - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store/Index/5", path); - Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); - } - - [Fact] - public void AttributeRoute_GenerateLink_NoMatch_WithConstraint() - { - // Arrange - var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { "id", "5" }, - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Null(path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithMixedAmbientValues() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_Match_WithQueryString() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api/Store?id=5", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_ForwardsRouteGroup() - { - // Arrange - var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - - var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { AttributeRouting.RouteGroupKey, entry.RouteGroup }, - }; - - var next = new StubRouter(); - var route = CreateAttributeRoute(next, entry); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); - } - - [Fact] - public void AttributeRoute_GenerateLink_RejectedByFirstRoute() - { - // Arrange - var entry1 = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); - var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Index", controller = "Blog" }); - - var route = CreateAttributeRoute(entry1, entry2); - - var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api2/Blog", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_RejectedByHandler() - { - // Arrange - var entry1 = CreateGenerationEntry("api/Store", new { action = "Edit", controller = "Store" }); - var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Edit", controller = "Store" }); - - var next = new StubRouter(); - - var callCount = 0; - next.GenerationDelegate = (VirtualPathContext c) => - { - // Reject entry 1. - callCount++; - return !c.ProvidedValues.Contains(new KeyValuePair( - AttributeRouting.RouteGroupKey, - entry1.RouteGroup)); - }; - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext(new { action = "Edit", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("api2/Store", path); - Assert.Equal(2, callCount); - } - - [Fact] - public void AttributeRoute_GenerateLink_ToArea() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 1; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 2; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("Help/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_ToArea_PredecedenceReversed() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 2; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 1; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("Help/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_ToArea_WithAmbientValues() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 1; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 2; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext( - values: new { action = "Edit", controller = "Store" }, - ambientValues: new { area = "Help" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("Help/Store", path); - } - - [Fact] - public void AttributeRoute_GenerateLink_OutOfArea_IgnoresAmbientValue() - { - // Arrange - var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 1; - - var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 2; - - var next = new StubRouter(); - - var route = CreateAttributeRoute(next, entry1, entry2); - - var context = CreateVirtualPathContext( - values: new { action = "Edit", controller = "Store" }, - ambientValues: new { area = "Blog" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal("Store", path); - } - - public static IEnumerable OptionalParamValues - { - get - { - return new object[][] - { - // defaults - // ambient values - // values - new object[] - { - "Test/{val1}/{val2}.{val3?}", - new {val1 = "someval1", val2 = "someval2", val3 = "someval3a"}, - new {val3 = "someval3v"}, - "Test/someval1/someval2.someval3v", - }, - new object[] - { - "Test/{val1}/{val2}.{val3?}", - new {val3 = "someval3a"}, - new {val1 = "someval1", val2 = "someval2", val3 = "someval3v" }, - "Test/someval1/someval2.someval3v", - }, - new object[] - { - "Test/{val1}/{val2}.{val3?}", - null, - new {val1 = "someval1", val2 = "someval2" }, - "Test/someval1/someval2", - }, - new object[] - { - "Test/{val1}.{val2}.{val3}.{val4?}", - new {val1 = "someval1", val2 = "someval2" }, - new {val4 = "someval4", val3 = "someval3" }, - "Test/someval1.someval2.someval3.someval4", - }, - new object[] - { - "Test/{val1}.{val2}.{val3}.{val4?}", - new {val1 = "someval1", val2 = "someval2" }, - new {val3 = "someval3" }, - "Test/someval1.someval2.someval3", - }, - new object[] - { - "Test/.{val2?}", - null, - new {val2 = "someval2" }, - "Test/.someval2", - }, - new object[] - { - "Test/.{val2?}", - null, - null, - "Test/", - }, - new object[] - { - "Test/{val1}.{val2}", - new {val1 = "someval1", val2 = "someval2" }, - new {val3 = "someval3" }, - "Test/someval1.someval2?val3=someval3", - }, - }; - } - } - - [Theory] - [MemberData("OptionalParamValues")] - public void AttributeRoute_GenerateLink_Match_WithOptionalParameters( - string template, - object ambientValues, - object values, - string expected) - { - // Arrange - var entry = CreateGenerationEntry(template, null); - var route = CreateAttributeRoute(entry); - - var context = CreateVirtualPathContext(values, ambientValues); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Equal(expected, path); - } - - [Fact] - public async Task AttributeRoute_CreatesNewRouteData() - { - // Arrange - RouteData nestedRouteData = null; - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - nestedRouteData = c.RouteData; - c.IsHandled = true; - }) - .Returns(Task.FromResult(true)); - - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); - - var context = CreateRouteContext("/api/Store"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("action", "Index"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotSame(originalRouteData, context.RouteData); - Assert.NotSame(originalRouteData, nestedRouteData); - Assert.Same(nestedRouteData, context.RouteData); - - // The new routedata is a copy - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Single(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - - Assert.IsType(context.RouteData.Routers[0]); - Assert.Same(next.Object, context.RouteData.Routers[1]); - } - - [Fact] - public async Task AttributeRoute_CreatesNewRouteData_ResetsWhenNotMatched() - { - // Arrange - RouteData nestedRouteData = null; - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - nestedRouteData = c.RouteData; - c.IsHandled = false; - }) - .Returns(Task.FromResult(true)); - - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); - - var context = CreateRouteContext("/api/Store"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("action", "Index"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Same(originalRouteData, context.RouteData); - Assert.NotSame(originalRouteData, nestedRouteData); - Assert.NotSame(nestedRouteData, context.RouteData); - - // The new routedata is a copy - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Equal("Index", nestedRouteData.Values["action"]); - Assert.None(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - Assert.Single(nestedRouteData.Values, kvp => kvp.Key == "test_route_group"); - - Assert.Empty(context.RouteData.Routers); - - Assert.IsType(nestedRouteData.Routers[0]); - Assert.Same(next.Object, nestedRouteData.Routers[1]); - } - - [Fact] - public async Task AttributeRoute_CreatesNewRouteData_ResetsWhenThrows() - { - // Arrange - RouteData nestedRouteData = null; - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback((c) => - { - nestedRouteData = c.RouteData; - c.IsHandled = false; - }) - .Throws(new Exception()); - - var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); - var route = CreateAttributeRoute(next.Object, entry); - - var context = CreateRouteContext("/api/Store"); - - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("action", "Index"); - - // Act - await Assert.ThrowsAsync(() => route.RouteAsync(context)); - - // Assert - Assert.Same(originalRouteData, context.RouteData); - Assert.NotSame(originalRouteData, nestedRouteData); - Assert.NotSame(nestedRouteData, context.RouteData); - - // The new routedata is a copy - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Equal("Index", nestedRouteData.Values["action"]); - Assert.None(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - Assert.Single(nestedRouteData.Values, kvp => kvp.Key == "test_route_group"); - - Assert.Empty(context.RouteData.Routers); - - Assert.IsType(nestedRouteData.Routers[0]); - Assert.Same(next.Object, nestedRouteData.Routers[1]); - } - - private static RouteContext CreateRouteContext(string requestPath) - { - var request = new Mock(MockBehavior.Strict); - request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); - - var context = new Mock(MockBehavior.Strict); - context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + handler.Object, + actionDescriptorsProvider.Object, + Mock.Of(), + NullLoggerFactory.Instance); + + var requestServices = new Mock(MockBehavior.Strict); + requestServices + .Setup(s => s.GetService(typeof(ILoggerFactory))) .Returns(NullLoggerFactory.Instance); - context.SetupGet(c => c.Request).Returns(request.Object); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Path = new PathString("/api/Store/Buy/5"); + httpContext.RequestServices = requestServices.Object; - return new RouteContext(context.Object); - } + var context = new RouteContext(httpContext); - private static VirtualPathContext CreateVirtualPathContext( - object values, - object ambientValues = null, - string name = null) - { - var mockHttpContext = new Mock(); - mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory))) - .Returns(NullLoggerFactory.Instance); + // Act 1 + await route.RouteAsync(context); - return new VirtualPathContext( - mockHttpContext.Object, - new RouteValueDictionary(ambientValues), - new RouteValueDictionary(values), - name); - } + // Assert 1 + Assert.True(context.IsHandled); + Assert.Equal("5", context.RouteData.Values["id"]); + Assert.Equal("2", context.RouteData.Values[AttributeRouting.RouteGroupKey]); - private static AttributeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order) - { - var routeGroup = string.Format("{0}&&{1}", order, template); + handler.Verify(h => h.RouteAsync(It.IsAny()), Times.Once()); - var entry = new AttributeRouteMatchingEntry(); - entry.Route = new TemplateRoute( - target: router, - routeTemplate: template, - defaults: new RouteValueDictionary(new { test_route_group = routeGroup }), - constraints: null, - dataTokens: null, - inlineConstraintResolver: CreateConstraintResolver()); + // Arrange 2 - remove the action and update the collection + actionDescriptors.RemoveAt(1); + actionDescriptorsProvider + .SetupGet(ad => ad.ActionDescriptors) + .Returns(new ActionDescriptorsCollection(actionDescriptors, version: 2)); - var routeTemplate = TemplateParser.Parse(template); - entry.Precedence = AttributeRoutePrecedence.Compute(routeTemplate); - entry.Order = order; + context = new RouteContext(httpContext); - return entry; - } + // Act 2 + await route.RouteAsync(context); - private static AttributeRouteLinkGenerationEntry CreateGenerationEntry( - string template, - object requiredValues, - int order = 0, - string name = null) - { - var constraintResolver = CreateConstraintResolver(); + // Assert 2 + Assert.False(context.IsHandled); + Assert.Empty(context.RouteData.Values); - var entry = new AttributeRouteLinkGenerationEntry(); - entry.TemplateText = template; - entry.Template = TemplateParser.Parse(template); - - var defaults = entry.Template.Parameters - .Where(p => p.DefaultValue != null) - .ToDictionary(p => p.Name, p => p.DefaultValue); - - var constraintBuilder = new RouteConstraintBuilder(CreateConstraintResolver(), template); - foreach (var parameter in entry.Template.Parameters) - { - if (parameter.InlineConstraints != null) - { - if (parameter.IsOptional) - { - constraintBuilder.SetOptional(parameter.Name); - } - - foreach (var constraint in parameter.InlineConstraints) - { - constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); - } - } - } - - var constraints = constraintBuilder.Build(); - - entry.Constraints = constraints; - entry.Defaults = defaults; - entry.Binder = new TemplateBinder(entry.Template, defaults); - entry.Order = order; - entry.Precedence = AttributeRoutePrecedence.Compute(entry.Template); - entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); - entry.RouteGroup = CreateRouteGroup(order, template); - entry.Name = name; - return entry; - } - - private AttributeRouteMatchingEntry CreateMatchingEntry(string template) - { - var mockConstraint = new Mock(); - mockConstraint.Setup(c => c.Match( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>(), - It.IsAny())) - .Returns(true); - - var mockConstraintResolver = new Mock(); - mockConstraintResolver.Setup(r => r.ResolveConstraint( - It.IsAny())) - .Returns(mockConstraint.Object); - - var entry = new AttributeRouteMatchingEntry() - { - Route = new TemplateRoute(new StubRouter(), template, mockConstraintResolver.Object) - }; - - return entry; - } - - private static string CreateRouteGroup(int order, string template) - { - return string.Format("{0}&{1}", order, template); - } - - private static DefaultInlineConstraintResolver CreateConstraintResolver() - { - var options = new RouteOptions(); - var optionsMock = new Mock>(); - optionsMock.SetupGet(o => o.Options).Returns(options); - - return new DefaultInlineConstraintResolver(optionsMock.Object); - } - - private static AttributeRoute CreateAttributeRoute(AttributeRouteLinkGenerationEntry entry) - { - return CreateAttributeRoute(new StubRouter(), entry); - } - - private static AttributeRoute CreateAttributeRoute(IRouter next, AttributeRouteLinkGenerationEntry entry) - { - return CreateAttributeRoute(next, new[] { entry }); - } - - private static AttributeRoute CreateAttributeRoute(params AttributeRouteLinkGenerationEntry[] entries) - { - return CreateAttributeRoute(new StubRouter(), entries); - } - - private static AttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteLinkGenerationEntry[] entries) - { - return new AttributeRoute( - next, - Enumerable.Empty(), - entries, - NullLoggerFactory.Instance); - } - - private static AttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteMatchingEntry[] entries) - { - return new AttributeRoute( - next, - entries, - Enumerable.Empty(), - NullLoggerFactory.Instance); - } - - private static AttributeRoute CreateRoutingAttributeRoute(ILoggerFactory loggerFactory = null, params AttributeRouteMatchingEntry[] entries) - { - loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; - - return new AttributeRoute( - new StubRouter(), - entries, - Enumerable.Empty(), - loggerFactory); - } - - private class StubRouter : IRouter - { - public VirtualPathContext GenerationContext { get; set; } - - public Func GenerationDelegate { get; set; } - - public RouteContext MatchingContext { get; set; } - - public Func MatchingDelegate { get; set; } - - public string GetVirtualPath(VirtualPathContext context) - { - GenerationContext = context; - - if (GenerationDelegate == null) - { - context.IsBound = true; - } - else - { - context.IsBound = GenerationDelegate(context); - } - - return null; - } - - public Task RouteAsync(RouteContext context) - { - if (MatchingDelegate == null) - { - context.IsHandled = true; - } - else - { - context.IsHandled = MatchingDelegate(context); - } - - return Task.FromResult(true); - } + handler.Verify(h => h.RouteAsync(It.IsAny()), Times.Once()); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs index fa66d12553..2d1122d9fe 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/AttributeRoutingTest.cs @@ -4,7 +4,10 @@ #if ASPNET50 using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Core; using Microsoft.AspNet.Routing; +using Microsoft.Framework.Logging; using Microsoft.Framework.OptionsModel; using Moq; using Xunit; @@ -29,12 +32,14 @@ namespace Microsoft.AspNet.Mvc.Routing "and can occur only at the start of the parameter." + Environment.NewLine + "Parameter name: routeTemplate"; - var router = CreateRouter(); + var handler = CreateRouter(); var services = CreateServices(action); // Act & Assert - var ex = Assert.Throws( - () => { AttributeRouting.CreateAttributeMegaRoute(router, services); }); + var ex = Assert.Throws(() => + { + AttributeRouting.CreateAttributeMegaRoute(handler, services); + }); Assert.Equal(expectedMessage, ex.Message); } @@ -53,12 +58,14 @@ namespace Microsoft.AspNet.Mvc.Routing "Error: The attribute route '{foo}/{action}' cannot contain a parameter named '{foo}'. " + "Use '[foo]' in the route template to insert the value 'bleh'."; - var router = CreateRouter(); + var handler = CreateRouter(); var services = CreateServices(action); // Act & Assert - var ex = Assert.Throws( - () => { AttributeRouting.CreateAttributeMegaRoute(router, services); }); + var ex = Assert.Throws(() => + { + AttributeRouting.CreateAttributeMegaRoute(handler, services); + }); Assert.Equal(expectedMessage, ex.Message); } @@ -84,18 +91,20 @@ namespace Microsoft.AspNet.Mvc.Routing "Error: The attribute route 'cool/{action}' cannot contain a parameter named '{action}'. " + "Use '[action]' in the route template to insert the value 'hey'."; - var router = CreateRouter(); + var handler = CreateRouter(); var services = CreateServices(action1, action2); // Act & Assert - var ex = Assert.Throws( - () => { AttributeRouting.CreateAttributeMegaRoute(router, services); }); + var ex = Assert.Throws(() => + { + AttributeRouting.CreateAttributeMegaRoute(handler, services); + }); Assert.Equal(expectedMessage, ex.Message); } [Fact] - public void AttributeRouting_WithReflectedActionDescriptor() + public void AttributeRouting_WithControllerActionDescriptor() { // Arrange var controllerType = typeof(HomeController); @@ -121,12 +130,14 @@ namespace Microsoft.AspNet.Mvc.Routing "Error: The attribute route '{controller}/{action}' cannot contain a parameter named '{controller}'. " + "Use '[controller]' in the route template to insert the value 'Home'."; - var router = CreateRouter(); + var handler = CreateRouter(); var services = CreateServices(action); // Act & Assert - var ex = Assert.Throws( - () => { AttributeRouting.CreateAttributeMegaRoute(router, services); }); + var ex = Assert.Throws(() => + { + AttributeRouting.CreateAttributeMegaRoute(handler, services); + }); Assert.Equal(expectedMessage, ex.Message); } @@ -172,6 +183,10 @@ namespace Microsoft.AspNet.Mvc.Routing .Setup(s => s.GetService(typeof(IInlineConstraintResolver))) .Returns(new DefaultInlineConstraintResolver(routeOptions.Object)); + services + .Setup(s => s.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); + return services.Object; } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Routing/InnerAttributeRouteTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/InnerAttributeRouteTest.cs new file mode 100644 index 0000000000..f7bbfb1618 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Routing/InnerAttributeRouteTest.cs @@ -0,0 +1,1759 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.Logging; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Template; +using Microsoft.Framework.Logging; +using Microsoft.Framework.OptionsModel; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Routing +{ + public class InnerAttributeRouteTest + { + [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) + { + // Arrange + var expectedRouteGroup = string.Format("{0}&&{1}", 0, firstTemplate); + + // 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + var context = CreateRouteContext(request); + + // Act + await route.RouteAsync(context); + + // Assert + if (expectedResult) + { + 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 matchingEntries = new[] { firstRoute }; + var linkGenerationEntries = Enumerable.Empty(); + var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); + 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 matchingEntries = new[] { firstRoute }; + var linkGenerationEntries = Enumerable.Empty(); + var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 = CreateAttributeRoute(next.Object, matchingRoutes, linkGenerationEntries); + + 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 NamedEntriesWithDifferentTemplates + { + get + { + var data = new TheoryData>(); + data.Add(new[] + { + CreateGenerationEntry("template", null, 0, "NamedEntry"), + CreateGenerationEntry("otherTemplate", null, 0, "NamedEntry"), + CreateGenerationEntry("anotherTemplate", null, 0, "NamedEntry") + }); + + // Default values for parameters are taken into account by comparing the templates. + data.Add(new[] + { + CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"), + CreateGenerationEntry("template/{parameter=1}", null, 0, "NamedEntry"), + CreateGenerationEntry("template/{parameter=2}", null, 0, "NamedEntry") + }); + + // Names for entries are compared ignoring casing. + data.Add(new[] + { + CreateGenerationEntry("template/{*parameter:int=0}", null, 0, "NamedEntry"), + CreateGenerationEntry("template/{*parameter:int=1}", null, 0, "NAMEDENTRY"), + CreateGenerationEntry("template/{*parameter:int=2}", null, 0, "namedentry") + }); + return data; + } + } + + [Theory] + [MemberData(nameof(InnerAttributeRouteTest.NamedEntriesWithDifferentTemplates))] + public void AttributeRoute_CreateAttributeRoute_ThrowsIfDifferentEntriesHaveTheSameName( + IEnumerable namedEntries) + { + // Arrange + string expectedExceptionMessage = "Two or more routes named 'NamedEntry' have different templates." + + Environment.NewLine + + "Parameter name: linkGenerationEntries"; + + var next = new Mock().Object; + + var matchingEntries = Enumerable.Empty(); + + // Act + var exception = Assert.Throws( + "linkGenerationEntries", + () => new InnerAttributeRoute( + next, + matchingEntries, + namedEntries, + NullLogger.Instance, + NullLogger.Instance, + version: 1)); + + Assert.Equal(expectedExceptionMessage, exception.Message, StringComparer.OrdinalIgnoreCase); + } + + public static IEnumerable NamedEntriesWithTheSameTemplate + { + get + { + var data = new TheoryData>(); + + data.Add(new[] + { + CreateGenerationEntry("template", null, 0, "NamedEntry"), + CreateGenerationEntry("template", null, 1, "NamedEntry"), + CreateGenerationEntry("template", null, 2, "NamedEntry") + }); + + // Templates are compared ignoring casing. + data.Add(new[] + { + CreateGenerationEntry("template", null, 0, "NamedEntry"), + CreateGenerationEntry("Template", null, 1, "NamedEntry"), + CreateGenerationEntry("TEMPLATE", null, 2, "NamedEntry") + }); + + data.Add(new[] + { + CreateGenerationEntry("template/{parameter=0}", null, 0, "NamedEntry"), + CreateGenerationEntry("template/{parameter=0}", null, 1, "NamedEntry"), + CreateGenerationEntry("template/{parameter=0}", null, 2, "NamedEntry") + }); + + return data; + } + } + + [Theory] + [MemberData(nameof(InnerAttributeRouteTest.NamedEntriesWithTheSameTemplate))] + public void AttributeRoute_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate( + IEnumerable namedEntries) + { + // Arrange + var expectedLink = namedEntries.First().Template.Parameters.Any() ? "template/5" : "template"; + + var expectedGroup = "0&" + namedEntries.First().TemplateText; + string selectedGroup = null; + var next = new Mock(); + next.Setup(s => s.GetVirtualPath(It.IsAny())) + .Callback(vpc => + { + vpc.IsBound = true; + selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; + }); + + var matchingEntries = Enumerable.Empty(); + + var route = CreateAttributeRoute(next.Object, matchingEntries, namedEntries); + + var ambientValues = namedEntries.First().Template.Parameters.Any() ? new { parameter = 5 } : null; + + var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedEntry"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedGroup, selectedGroup); + Assert.Equal(expectedLink, result); + } + + [Fact] + public void AttributeRoute_GenerateLink_WithName() + { + // Arrange + string selectedGroup = null; + var next = new Mock(); + next.Setup(s => s.GetVirtualPath(It.IsAny())) + .Callback(vpc => + { + vpc.IsBound = true; + selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; + }); + + var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute"); + var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; + + var matchingEntries = Enumerable.Empty(); + + var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); + + var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal("1&named", selectedGroup); + Assert.Equal("named", result); + } + + [Fact] + public void AttributeRoute_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName() + { + // Arrange + string selectedGroup = null; + var next = new Mock(); + next.Setup(s => s.GetVirtualPath(It.IsAny())) + .Callback(vpc => + { + vpc.IsBound = true; + selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; + }); + + var namedEntry = CreateGenerationEntry("named", requiredValues: null, order: 1, name: "NamedRoute"); + + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; + + var matchingEntries = Enumerable.Empty(); + + var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); + + var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("template/{parameter:int}", null)] + [InlineData("template/{parameter:int}", "NaN")] + [InlineData("template/{parameter}", null)] + [InlineData("template/{*parameter:int}", null)] + [InlineData("template/{*parameter:int}", "NaN")] + public void AttributeRoute_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value) + { + // Arrange + string selectedGroup = null; + var next = new Mock(); + next.Setup(s => s.GetVirtualPath(It.IsAny())) + .Callback(vpc => + { + vpc.IsBound = true; + selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; + }); + + var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute"); + + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; + + var matchingEntries = Enumerable.Empty(); + + var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); + + var ambientValues = value == null ? null : new { parameter = value }; + + var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("template/{parameter:int}", "5")] + [InlineData("template/{parameter}", "5")] + [InlineData("template/{*parameter:int}", "5")] + [InlineData("template/{*parameter}", "5")] + public void AttributeRoute_GeneratesLink_IfValuesMatchNamedEntry(string template, string value) + { + // Arrange + string selectedGroup = null; + var next = new Mock(); + next.Setup(s => s.GetVirtualPath(It.IsAny())) + .Callback(vpc => + { + vpc.IsBound = true; + selectedGroup = (string)vpc.ProvidedValues[AttributeRouting.RouteGroupKey]; + }); + + var namedEntry = CreateGenerationEntry(template, requiredValues: null, order: 1, name: "NamedRoute"); + + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + var unnamedEntry = CreateGenerationEntry("unnamed", requiredValues: null, order: 0); + + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + var linkGenerationEntries = new[] { namedEntry, unnamedEntry }; + + var matchingEntries = Enumerable.Empty(); + + var route = CreateAttributeRoute(next.Object, matchingEntries, linkGenerationEntries); + + var ambientValues = value == null ? null : new { parameter = value }; + + var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal(string.Format("1&{0}", template), selectedGroup); + Assert.Equal("template/5", result); + } + + [Fact] + public async void AttributeRoute_RouteAsyncHandled_LogsCorrectValues() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink); + + var entry = CreateMatchingEntry("api/Store"); + var route = CreateRoutingAttributeRoute(loggerFactory, entry); + + var context = CreateRouteContext("/api/Store"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(AttributeRoute).FullName, scope.LoggerName); + Assert.Equal("AttributeRoute.RouteAsync", scope.Scope); + + Assert.Equal(1, sink.Writes.Count); + + var write = sink.Writes[0]; + Assert.Equal(typeof(AttributeRoute).FullName, write.LoggerName); + Assert.Equal("AttributeRoute.RouteAsync", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("AttributeRoute.RouteAsync", values.Name); + Assert.True(values.Handled); + } + + [Fact] + public async void AttributeRoute_RouteAsyncNotHandled_LogsCorrectValues() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink); + + var entry = CreateMatchingEntry("api/Store"); + var route = CreateRoutingAttributeRoute(loggerFactory, entry); + + var context = CreateRouteContext("/"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Equal(1, sink.Scopes.Count); + var scope = sink.Scopes[0]; + Assert.Equal(typeof(AttributeRoute).FullName, scope.LoggerName); + Assert.Equal("AttributeRoute.RouteAsync", scope.Scope); + + Assert.Equal(1, sink.Writes.Count); + + var write = sink.Writes[0]; + Assert.Equal(typeof(AttributeRoute).FullName, write.LoggerName); + Assert.Equal("AttributeRoute.RouteAsync", write.Scope); + var values = Assert.IsType(write.State); + Assert.Equal("AttributeRoute.RouteAsync", values.Name); + Assert.False(values.Handled); + } + + [Fact] + public void AttributeRoute_GenerateLink_NoRequiredValues() + { + // Arrange + var entry = CreateGenerationEntry("api/Store", new { }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match() + { + // Arrange + var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_NoMatch() + { + // Arrange + var entry = CreateGenerationEntry("api/Store", new { action = "Details", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Null(path); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match_WithAmbientValues() + { + // Arrange + var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match_WithParameters() + { + // Arrange + var entry = CreateGenerationEntry("api/Store/{action}", new { action = "Index", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store/Index", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match_WithMoreParameters() + { + // Arrange + var entry = CreateGenerationEntry( + "api/{area}/dosomething/{controller}/{action}", + new { action = "Index", controller = "Store", area = "AwesomeCo" }); + + var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "area", "AwesomeCo" }, + { "controller", "Store" }, + { "action", "Index" }, + { AttributeRouting.RouteGroupKey, entry.RouteGroup }, + }; + + var next = new StubRouter(); + var route = CreateAttributeRoute(next, entry); + + var context = CreateVirtualPathContext( + new { action = "Index", controller = "Store" }, + new { area = "AwesomeCo" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/AwesomeCo/dosomething/Store/Index", path); + Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match_WithDefault() + { + // Arrange + var entry = CreateGenerationEntry("api/Store/{action=Index}", new { action = "Index", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match_WithConstraint() + { + // Arrange + var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + + var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "action", "Index" }, + { "id", 5 }, + { AttributeRouting.RouteGroupKey, entry.RouteGroup }, + }; + + var next = new StubRouter(); + var route = CreateAttributeRoute(next, entry); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store/Index/5", path); + Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); + } + + [Fact] + public void AttributeRoute_GenerateLink_NoMatch_WithConstraint() + { + // Arrange + var entry = CreateGenerationEntry("api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "id", "5" }, + { AttributeRouting.RouteGroupKey, entry.RouteGroup }, + }; + + var next = new StubRouter(); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Null(path); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match_WithMixedAmbientValues() + { + // Arrange + var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_Match_WithQueryString() + { + // Arrange + var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api/Store?id=5", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_ForwardsRouteGroup() + { + // Arrange + var entry = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); + + var expectedValues = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { AttributeRouting.RouteGroupKey, entry.RouteGroup }, + }; + + var next = new StubRouter(); + var route = CreateAttributeRoute(next, entry); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal(expectedValues, next.GenerationContext.ProvidedValues); + } + + [Fact] + public void AttributeRoute_GenerateLink_RejectedByFirstRoute() + { + // Arrange + var entry1 = CreateGenerationEntry("api/Store", new { action = "Index", controller = "Store" }); + var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Index", controller = "Blog" }); + + var route = CreateAttributeRoute(entry1, entry2); + + var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api2/Blog", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_RejectedByHandler() + { + // Arrange + var entry1 = CreateGenerationEntry("api/Store", new { action = "Edit", controller = "Store" }); + var entry2 = CreateGenerationEntry("api2/{controller}", new { action = "Edit", controller = "Store" }); + + var next = new StubRouter(); + + var callCount = 0; + next.GenerationDelegate = (VirtualPathContext c) => + { + // Reject entry 1. + callCount++; + return !c.ProvidedValues.Contains(new KeyValuePair( + AttributeRouting.RouteGroupKey, + entry1.RouteGroup)); + }; + + var route = CreateAttributeRoute(next, entry1, entry2); + + var context = CreateVirtualPathContext(new { action = "Edit", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("api2/Store", path); + Assert.Equal(2, callCount); + } + + [Fact] + public void AttributeRoute_GenerateLink_ToArea() + { + // Arrange + var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 1; + + var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 2; + + var next = new StubRouter(); + + var route = CreateAttributeRoute(next, entry1, entry2); + + var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("Help/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_ToArea_PredecedenceReversed() + { + // Arrange + var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; + + var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; + + var next = new StubRouter(); + + var route = CreateAttributeRoute(next, entry1, entry2); + + var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("Help/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_ToArea_WithAmbientValues() + { + // Arrange + var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 1; + + var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 2; + + var next = new StubRouter(); + + var route = CreateAttributeRoute(next, entry1, entry2); + + var context = CreateVirtualPathContext( + values: new { action = "Edit", controller = "Store" }, + ambientValues: new { area = "Help" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("Help/Store", path); + } + + [Fact] + public void AttributeRoute_GenerateLink_OutOfArea_IgnoresAmbientValue() + { + // Arrange + var entry1 = CreateGenerationEntry("Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 1; + + var entry2 = CreateGenerationEntry("Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 2; + + var next = new StubRouter(); + + var route = CreateAttributeRoute(next, entry1, entry2); + + var context = CreateVirtualPathContext( + values: new { action = "Edit", controller = "Store" }, + ambientValues: new { area = "Blog" }); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal("Store", path); + } + + public static IEnumerable OptionalParamValues + { + get + { + return new object[][] + { + // defaults + // ambient values + // values + new object[] + { + "Test/{val1}/{val2}.{val3?}", + new {val1 = "someval1", val2 = "someval2", val3 = "someval3a"}, + new {val3 = "someval3v"}, + "Test/someval1/someval2.someval3v", + }, + new object[] + { + "Test/{val1}/{val2}.{val3?}", + new {val3 = "someval3a"}, + new {val1 = "someval1", val2 = "someval2", val3 = "someval3v" }, + "Test/someval1/someval2.someval3v", + }, + new object[] + { + "Test/{val1}/{val2}.{val3?}", + null, + new {val1 = "someval1", val2 = "someval2" }, + "Test/someval1/someval2", + }, + new object[] + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new {val1 = "someval1", val2 = "someval2" }, + new {val4 = "someval4", val3 = "someval3" }, + "Test/someval1.someval2.someval3.someval4", + }, + new object[] + { + "Test/{val1}.{val2}.{val3}.{val4?}", + new {val1 = "someval1", val2 = "someval2" }, + new {val3 = "someval3" }, + "Test/someval1.someval2.someval3", + }, + new object[] + { + "Test/.{val2?}", + null, + new {val2 = "someval2" }, + "Test/.someval2", + }, + new object[] + { + "Test/.{val2?}", + null, + null, + "Test/", + }, + new object[] + { + "Test/{val1}.{val2}", + new {val1 = "someval1", val2 = "someval2" }, + new {val3 = "someval3" }, + "Test/someval1.someval2?val3=someval3", + }, + }; + } + } + + [Theory] + [MemberData("OptionalParamValues")] + public void AttributeRoute_GenerateLink_Match_WithOptionalParameters( + string template, + object ambientValues, + object values, + string expected) + { + // Arrange + var entry = CreateGenerationEntry(template, null); + var route = CreateAttributeRoute(entry); + + var context = CreateVirtualPathContext(values, ambientValues); + + // Act + var path = route.GetVirtualPath(context); + + // Assert + Assert.Equal(expected, path); + } + + [Fact] + public async Task AttributeRoute_CreatesNewRouteData() + { + // Arrange + RouteData nestedRouteData = null; + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback((c) => + { + nestedRouteData = c.RouteData; + c.IsHandled = true; + }) + .Returns(Task.FromResult(true)); + + var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); + var route = CreateAttributeRoute(next.Object, entry); + + var context = CreateRouteContext("/api/Store"); + + var originalRouteData = context.RouteData; + originalRouteData.Values.Add("action", "Index"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotSame(originalRouteData, context.RouteData); + Assert.NotSame(originalRouteData, nestedRouteData); + Assert.Same(nestedRouteData, context.RouteData); + + // The new routedata is a copy + Assert.Equal("Index", context.RouteData.Values["action"]); + Assert.Single(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); + + Assert.IsType(context.RouteData.Routers[0]); + Assert.Same(next.Object, context.RouteData.Routers[1]); + } + + [Fact] + public async Task AttributeRoute_CreatesNewRouteData_ResetsWhenNotMatched() + { + // Arrange + RouteData nestedRouteData = null; + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback((c) => + { + nestedRouteData = c.RouteData; + c.IsHandled = false; + }) + .Returns(Task.FromResult(true)); + + var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); + var route = CreateAttributeRoute(next.Object, entry); + + var context = CreateRouteContext("/api/Store"); + + var originalRouteData = context.RouteData; + originalRouteData.Values.Add("action", "Index"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Same(originalRouteData, context.RouteData); + Assert.NotSame(originalRouteData, nestedRouteData); + Assert.NotSame(nestedRouteData, context.RouteData); + + // The new routedata is a copy + Assert.Equal("Index", context.RouteData.Values["action"]); + Assert.Equal("Index", nestedRouteData.Values["action"]); + Assert.None(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); + Assert.Single(nestedRouteData.Values, kvp => kvp.Key == "test_route_group"); + + Assert.Empty(context.RouteData.Routers); + + Assert.IsType(nestedRouteData.Routers[0]); + Assert.Same(next.Object, nestedRouteData.Routers[1]); + } + + [Fact] + public async Task AttributeRoute_CreatesNewRouteData_ResetsWhenThrows() + { + // Arrange + RouteData nestedRouteData = null; + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback((c) => + { + nestedRouteData = c.RouteData; + c.IsHandled = false; + }) + .Throws(new Exception()); + + var entry = CreateMatchingEntry(next.Object, "api/Store", order: 0); + var route = CreateAttributeRoute(next.Object, entry); + + var context = CreateRouteContext("/api/Store"); + + var originalRouteData = context.RouteData; + originalRouteData.Values.Add("action", "Index"); + + // Act + await Assert.ThrowsAsync(() => route.RouteAsync(context)); + + // Assert + Assert.Same(originalRouteData, context.RouteData); + Assert.NotSame(originalRouteData, nestedRouteData); + Assert.NotSame(nestedRouteData, context.RouteData); + + // The new routedata is a copy + Assert.Equal("Index", context.RouteData.Values["action"]); + Assert.Equal("Index", nestedRouteData.Values["action"]); + Assert.None(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); + Assert.Single(nestedRouteData.Values, kvp => kvp.Key == "test_route_group"); + + Assert.Empty(context.RouteData.Routers); + + Assert.IsType(nestedRouteData.Routers[0]); + Assert.Same(next.Object, nestedRouteData.Routers[1]); + } + + private static RouteContext CreateRouteContext(string requestPath) + { + var request = new Mock(MockBehavior.Strict); + request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); + + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); + + context.SetupGet(c => c.Request).Returns(request.Object); + + return new RouteContext(context.Object); + } + + private static VirtualPathContext CreateVirtualPathContext( + object values, + object ambientValues = null, + string name = null) + { + var mockHttpContext = new Mock(); + mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); + + return new VirtualPathContext( + mockHttpContext.Object, + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values), + name); + } + + private static AttributeRouteMatchingEntry CreateMatchingEntry(IRouter router, string template, int order) + { + var routeGroup = string.Format("{0}&&{1}", order, template); + + var entry = new AttributeRouteMatchingEntry(); + entry.Route = new TemplateRoute( + target: router, + routeTemplate: template, + defaults: new RouteValueDictionary(new { test_route_group = routeGroup }), + constraints: null, + dataTokens: null, + inlineConstraintResolver: CreateConstraintResolver()); + + var routeTemplate = TemplateParser.Parse(template); + entry.Precedence = AttributeRoutePrecedence.Compute(routeTemplate); + entry.Order = order; + + return entry; + } + + private static AttributeRouteLinkGenerationEntry CreateGenerationEntry( + string template, + object requiredValues, + int order = 0, + string name = null) + { + var constraintResolver = CreateConstraintResolver(); + + var entry = new AttributeRouteLinkGenerationEntry(); + entry.TemplateText = template; + entry.Template = TemplateParser.Parse(template); + + var defaults = entry.Template.Parameters + .Where(p => p.DefaultValue != null) + .ToDictionary(p => p.Name, p => p.DefaultValue); + + var constraintBuilder = new RouteConstraintBuilder(CreateConstraintResolver(), template); + foreach (var parameter in entry.Template.Parameters) + { + if (parameter.InlineConstraints != null) + { + if (parameter.IsOptional) + { + constraintBuilder.SetOptional(parameter.Name); + } + + foreach (var constraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); + } + } + } + + var constraints = constraintBuilder.Build(); + + entry.Constraints = constraints; + entry.Defaults = defaults; + entry.Binder = new TemplateBinder(entry.Template, defaults); + entry.Order = order; + entry.Precedence = AttributeRoutePrecedence.Compute(entry.Template); + entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); + entry.RouteGroup = CreateRouteGroup(order, template); + entry.Name = name; + return entry; + } + + private AttributeRouteMatchingEntry CreateMatchingEntry(string template) + { + var mockConstraint = new Mock(); + mockConstraint.Setup(c => c.Match( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny())) + .Returns(true); + + var mockConstraintResolver = new Mock(); + mockConstraintResolver.Setup(r => r.ResolveConstraint( + It.IsAny())) + .Returns(mockConstraint.Object); + + var entry = new AttributeRouteMatchingEntry() + { + Route = new TemplateRoute(new StubRouter(), template, mockConstraintResolver.Object) + }; + + return entry; + } + + private static string CreateRouteGroup(int order, string template) + { + return string.Format("{0}&{1}", order, template); + } + + private static DefaultInlineConstraintResolver CreateConstraintResolver() + { + var options = new RouteOptions(); + var optionsMock = new Mock>(); + optionsMock.SetupGet(o => o.Options).Returns(options); + + return new DefaultInlineConstraintResolver(optionsMock.Object); + } + + private static InnerAttributeRoute CreateAttributeRoute(AttributeRouteLinkGenerationEntry entry) + { + return CreateAttributeRoute(new StubRouter(), entry); + } + + private static InnerAttributeRoute CreateAttributeRoute(IRouter next, AttributeRouteLinkGenerationEntry entry) + { + return CreateAttributeRoute(next, new[] { entry }); + } + + private static InnerAttributeRoute CreateAttributeRoute(params AttributeRouteLinkGenerationEntry[] entries) + { + return CreateAttributeRoute(new StubRouter(), entries); + } + + private static InnerAttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteLinkGenerationEntry[] entries) + { + return CreateAttributeRoute( + next, + Enumerable.Empty(), + entries); + } + + private static InnerAttributeRoute CreateAttributeRoute(IRouter next, params AttributeRouteMatchingEntry[] entries) + { + return CreateAttributeRoute( + next, + entries, + Enumerable.Empty()); + } + + private static InnerAttributeRoute CreateAttributeRoute( + IRouter next, + IEnumerable matchingEntries, + IEnumerable generationEntries) + { + return new InnerAttributeRoute( + next, + matchingEntries, + generationEntries, + NullLogger.Instance, + NullLogger.Instance, + version: 1); + } + + private static InnerAttributeRoute CreateRoutingAttributeRoute(ILoggerFactory loggerFactory = null, params AttributeRouteMatchingEntry[] entries) + { + loggerFactory = loggerFactory ?? NullLoggerFactory.Instance; + + return new InnerAttributeRoute( + new StubRouter(), + entries, + Enumerable.Empty(), + loggerFactory.Create(), + loggerFactory.Create(typeof(RouteConstraintMatcher).FullName), + version: 1); + } + + private class StubRouter : IRouter + { + public VirtualPathContext GenerationContext { get; set; } + + public Func GenerationDelegate { get; set; } + + public RouteContext MatchingContext { get; set; } + + public Func MatchingDelegate { get; set; } + + public string GetVirtualPath(VirtualPathContext context) + { + GenerationContext = context; + + if (GenerationDelegate == null) + { + context.IsBound = true; + } + else + { + context.IsBound = GenerationDelegate(context); + } + + return null; + } + + public Task RouteAsync(RouteContext context) + { + if (MatchingDelegate == null) + { + context.IsHandled = true; + } + else + { + context.IsHandled = MatchingDelegate(context); + } + + return Task.FromResult(true); + } + } + } +} \ No newline at end of file