// Copyright (c) .NET Foundation. 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.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Mvc.Internal { public class AttributeRoute : IRouter { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; private readonly IServiceProvider _services; private readonly Func _handlerFactory; private TreeRouter _router; public AttributeRoute( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IServiceProvider services, Func handlerFactory) { if (actionDescriptorCollectionProvider == null) { throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider)); } if (services == null) { throw new ArgumentNullException(nameof(services)); } if (handlerFactory == null) { _handlerFactory = handlerFactory; } _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; _services = services; _handlerFactory = handlerFactory; } /// public VirtualPathData GetVirtualPath(VirtualPathContext context) { var router = GetTreeRouter(); return router.GetVirtualPath(context); } /// public Task RouteAsync(RouteContext context) { var router = GetTreeRouter(); return router.RouteAsync(context); } private TreeRouter GetTreeRouter() { var actions = _actionDescriptorCollectionProvider.ActionDescriptors; // This is a safe-race. We'll never set router back to null after initializing // it on startup. if (_router == null || _router.Version != actions.Version) { var builder = _services.GetRequiredService(); AddEntries(builder, actions); _router = builder.Build(actions.Version); } return _router; } // internal for testing internal void AddEntries(TreeRouteBuilder builder, ActionDescriptorCollection actions) { var routeInfos = GetRouteInfos(actions.Items); // We're creating one TreeRouteLinkGenerationEntry per action. This allows us to match the intended // action by expected route values, and then use the TemplateBinder to generate the link. foreach (var routeInfo in routeInfos) { var defaults = new RouteValueDictionary(); foreach (var kvp in routeInfo.ActionDescriptor.RouteValues) { defaults.Add(kvp.Key, kvp.Value); } // We use the `NullRouter` as the route handler because we don't need to do anything for link // generations. The TreeRouter does it all for us. builder.MapOutbound( NullRouter.Instance, routeInfo.RouteTemplate, defaults, routeInfo.RouteName, routeInfo.Order); } // 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 groups = GroupRouteInfos(routeInfos); foreach (var group in groups) { var handler = _handlerFactory(group.ToArray()); // Note that because we only support 'inline' defaults, each routeInfo group also has the same // set of defaults. // // We then inject the route group as a default for the matcher so it gets passed back to MVC // for use in action selection. builder.MapInbound( handler, group.Key.RouteTemplate, group.Key.RouteName, group.Key.Order); } } private static IEnumerable> GroupRouteInfos(List routeInfos) { return routeInfos.GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance); } private static List GetRouteInfos(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?.Template != null); foreach (var action in attributeRoutedActions) { var routeInfo = GetRouteInfo(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( Dictionary templateCache, ActionDescriptor action) { var routeInfo = new RouteInfo() { ActionDescriptor = action, }; 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.RouteTemplate = parsedTemplate; } catch (Exception ex) { routeInfo.ErrorMessage = ex.Message; return routeInfo; } foreach (var kvp in action.RouteValues) { foreach (var parameter in routeInfo.RouteTemplate.Parameters) { if (string.Equals(kvp.Key, parameter.Name, StringComparison.OrdinalIgnoreCase)) { routeInfo.ErrorMessage = Resources.FormatAttributeRoute_CannotContainParameter( routeInfo.RouteTemplate.TemplateText, kvp.Key, kvp.Value); return routeInfo; } } } routeInfo.Order = action.AttributeRouteInfo.Order; routeInfo.RouteName = action.AttributeRouteInfo.Name; return routeInfo; } private class RouteInfo { public ActionDescriptor ActionDescriptor { get; set; } public string ErrorMessage { get; set; } public int Order { get; set; } public string RouteName { get; set; } public RouteTemplate RouteTemplate { get; set; } } private class RouteInfoEqualityComparer : IEqualityComparer { public static readonly RouteInfoEqualityComparer Instance = new RouteInfoEqualityComparer(); public bool Equals(RouteInfo x, RouteInfo y) { if (x == null && y == null) { return true; } else if (x == null ^ y == null) { return false; } else if (x.Order != y.Order) { return false; } else { return string.Equals( x.RouteTemplate.TemplateText, y.RouteTemplate.TemplateText, StringComparison.OrdinalIgnoreCase); } } public int GetHashCode(RouteInfo obj) { if (obj == null) { return 0; } var hash = new HashCodeCombiner(); hash.Add(obj.Order); hash.Add(obj.RouteTemplate.TemplateText, StringComparer.OrdinalIgnoreCase); return hash; } } // Used only to hook up link generation, and it doesn't need to do anything. private class NullRouter : IRouter { public static readonly NullRouter Instance = new NullRouter(); public VirtualPathData GetVirtualPath(VirtualPathContext context) { return null; } public Task RouteAsync(RouteContext context) { throw new NotImplementedException(); } } } }