// 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.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Mvc.Infrastructure; using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing.Template; using Microsoft.Extensions.Logging; namespace Microsoft.AspNet.Mvc.Routing { public class AttributeRoute : IRouter { private readonly IRouter _target; private readonly IActionDescriptorsCollectionProvider _actionDescriptorsCollectionProvider; private readonly IInlineConstraintResolver _constraintResolver; // 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; public AttributeRoute( IRouter target, IActionDescriptorsCollectionProvider actionDescriptorsCollectionProvider, IInlineConstraintResolver constraintResolver, ILoggerFactory loggerFactory) { if (target == null) { throw new ArgumentNullException(nameof(target)); } if (actionDescriptorsCollectionProvider == null) { throw new ArgumentNullException(nameof(actionDescriptorsCollectionProvider)); } if (constraintResolver == null) { throw new ArgumentNullException(nameof(constraintResolver)); } if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); } _target = target; _actionDescriptorsCollectionProvider = actionDescriptorsCollectionProvider; _constraintResolver = constraintResolver; _routeLogger = loggerFactory.CreateLogger(); _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); } /// public VirtualPathData GetVirtualPath(VirtualPathContext context) { 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) { _inner = BuildRoute(actions); } 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) { 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, Target = _target, RouteName = routeInfo.Name, RouteTemplate = routeInfo.RouteTemplate, TemplateMatcher = new TemplateMatcher( routeInfo.ParsedTemplate, new Dictionary(StringComparer.OrdinalIgnoreCase) { { AttributeRouting.RouteGroupKey, routeInfo.RouteGroup } }), Constraints = routeInfo.Constraints }); } return new InnerAttributeRoute( _target, matchingEntries, generationEntries, _routeLogger, _constraintLogger, actions.Version); } 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; } } } }