From b4d1eec87a0a1bc7f641301b8074c303962c2f20 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 29 Jan 2015 11:13:40 -0800 Subject: [PATCH] Makes attribute routing respond to AD updates This change makes the attribute route capable of responding to updates in the action descriptor collection. The change moves the actual route implementation to InnerAttributeRoute. AttributeRoute now wraps an InnerAttributeRoute - when processing a route, AttributeRoute will now check with action descriptor collection if its InnerAttributeRoute is still 'current' and rebuild it if necessary. --- .../Routing/AttributeRoute.cs | 450 +++-- .../AttributeRouteLinkGenerationEntry.cs | 4 +- .../Routing/AttributeRouteMatchingEntry.cs | 4 +- .../Routing/AttributeRouting.cs | 245 +-- .../Routing/InnerAttributeRoute.cs | 285 +++ .../Routing/AttributeRouteTest.cs | 1780 +---------------- .../Routing/AttributeRoutingTest.cs | 41 +- .../Routing/InnerAttributeRouteTest.cs | 1759 ++++++++++++++++ 8 files changed, 2392 insertions(+), 2176 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/Routing/InnerAttributeRoute.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Routing/InnerAttributeRouteTest.cs 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