| File: Routing\AttributeRoute.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; namespace Microsoft.AspNetCore.Mvc.Routing; internal sealed class AttributeRoute : IRouter { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; private readonly IServiceProvider _services; private readonly Func<ActionDescriptor[], IRouter> _handlerFactory; private TreeRouter? _router; public AttributeRoute( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, IServiceProvider services, Func<ActionDescriptor[], IRouter> handlerFactory) { ArgumentNullException.ThrowIfNull(actionDescriptorCollectionProvider); ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(handlerFactory); _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; _services = services; _handlerFactory = handlerFactory; } /// <inheritdoc /> public VirtualPathData GetVirtualPath(VirtualPathContext context) { var router = GetTreeRouter(); return router.GetVirtualPath(context); } /// <inheritdoc /> 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<TreeRouteBuilder>(); 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) { if (routeInfo.SuppressLinkGeneration) { continue; } var defaults = new RouteValueDictionary(); foreach (var kvp in routeInfo.ActionDescriptor.RouteValues) { defaults.Add(kvp.Key, kvp.Value); } try { // 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); } catch (RouteCreationException routeCreationException) { throw new RouteCreationException( "An error occurred while adding a route to the route builder. " + $"Route name '{routeInfo.RouteName}' and template '{routeInfo.RouteTemplate!.TemplateText}'.", routeCreationException); } } // 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 = GetInboundRouteGroups(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<IGrouping<RouteInfo, ActionDescriptor>> GetInboundRouteGroups(List<RouteInfo> routeInfos) { return routeInfos .Where(routeInfo => !routeInfo.SuppressPathMatching) .GroupBy(r => r, r => r.ActionDescriptor, RouteInfoEqualityComparer.Instance); } private static List<RouteInfo> GetRouteInfos(IReadOnlyList<ActionDescriptor> actions) { var routeInfos = new List<RouteInfo>(); var errors = new List<RouteInfo>(); // 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<string, RouteTemplate>(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 RouteCreationException(message); } return routeInfos; } private static RouteInfo GetRouteInfo( Dictionary<string, RouteTemplate> templateCache, ActionDescriptor action) { var routeInfo = new RouteInfo() { ActionDescriptor = action, }; try { var template = action.AttributeRouteInfo!.Template!; if (!templateCache.TryGetValue(template, out var parsedTemplate)) { // Parsing with throw if the template is invalid. parsedTemplate = TemplateParser.Parse(template); templateCache.Add(template, parsedTemplate); } routeInfo.RouteTemplate = parsedTemplate; routeInfo.SuppressPathMatching = action.AttributeRouteInfo.SuppressPathMatching; routeInfo.SuppressLinkGeneration = action.AttributeRouteInfo.SuppressLinkGeneration; } 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 sealed class RouteInfo { public ActionDescriptor ActionDescriptor { get; init; } = default!; public string? ErrorMessage { get; set; } public int Order { get; set; } public string? RouteName { get; set; } public RouteTemplate? RouteTemplate { get; set; } public bool SuppressPathMatching { get; set; } public bool SuppressLinkGeneration { get; set; } } private sealed class RouteInfoEqualityComparer : IEqualityComparer<RouteInfo> { public static readonly RouteInfoEqualityComparer Instance = new(); 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 HashCode(); hash.Add(obj.Order); hash.Add(obj.RouteTemplate!.TemplateText, StringComparer.OrdinalIgnoreCase); return hash.ToHashCode(); } } // Used only to hook up link generation, and it doesn't need to do anything. private sealed 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(); } } }