| File: RouteCollection.cs | Web Access |
| Project: src\aspnetcore\src\Http\Routing\src\Microsoft.AspNetCore.Routing.csproj (Microsoft.AspNetCore.Routing) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Routing; /// <summary> /// Supports managing a collection for multiple routes. /// </summary> public class RouteCollection : IRouteCollection { private readonly List<IRouter> _routes = new List<IRouter>(); private readonly List<IRouter> _unnamedRoutes = new List<IRouter>(); private readonly Dictionary<string, INamedRouter> _namedRoutes = new Dictionary<string, INamedRouter>(StringComparer.OrdinalIgnoreCase); private RouteOptions? _options; /// <summary> /// Gets the route at a given index. /// </summary> /// <value>The route at the given index.</value> public IRouter this[int index] { get { return _routes[index]; } } /// <summary> /// Gets the total number of routes registered in the collection. /// </summary> public int Count { get { return _routes.Count; } } /// <inheritdoc /> public void Add(IRouter router) { ArgumentNullException.ThrowIfNull(router); var namedRouter = router as INamedRouter; if (namedRouter != null) { if (!string.IsNullOrEmpty(namedRouter.Name)) { _namedRoutes.Add(namedRouter.Name, namedRouter); } } else { _unnamedRoutes.Add(router); } _routes.Add(router); } /// <inheritdoc /> public virtual async Task RouteAsync(RouteContext context) { // Perf: We want to avoid allocating a new RouteData for each route we need to process. // We can do this by snapshotting the state at the beginning and then restoring it // for each router we execute. var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null); for (var i = 0; i < Count; i++) { var route = this[i]; context.RouteData.Routers.Add(route); try { await route.RouteAsync(context); if (context.Handler != null) { break; } } finally { if (context.Handler == null) { snapshot.Restore(); } } } } /// <inheritdoc /> public virtual VirtualPathData? GetVirtualPath(VirtualPathContext context) { EnsureOptions(context.HttpContext); if (!string.IsNullOrEmpty(context.RouteName)) { VirtualPathData? namedRoutePathData = null; if (_namedRoutes.TryGetValue(context.RouteName, out var matchedNamedRoute)) { namedRoutePathData = matchedNamedRoute.GetVirtualPath(context); } var pathData = GetVirtualPath(context, _unnamedRoutes); // If the named route and one of the unnamed routes also matches, then we have an ambiguity. if (namedRoutePathData != null && pathData != null) { var message = Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName); throw new InvalidOperationException(message); } return NormalizeVirtualPath(namedRoutePathData ?? pathData); } else { return NormalizeVirtualPath(GetVirtualPath(context, _routes)); } } private static VirtualPathData? GetVirtualPath(VirtualPathContext context, List<IRouter> routes) { for (var i = 0; i < routes.Count; i++) { var route = routes[i]; var pathData = route.GetVirtualPath(context); if (pathData != null) { return pathData; } } return null; } private VirtualPathData? NormalizeVirtualPath(VirtualPathData? pathData) { if (pathData == null) { return pathData; } Debug.Assert(_options != null); var url = pathData.VirtualPath; if (!string.IsNullOrEmpty(url) && (_options.LowercaseUrls || _options.AppendTrailingSlash)) { var indexOfSeparator = url.AsSpan().IndexOfAny('?', '#'); var urlWithoutQueryString = url; var queryString = string.Empty; if (indexOfSeparator != -1) { urlWithoutQueryString = url.Substring(0, indexOfSeparator); queryString = url.Substring(indexOfSeparator); } if (_options.LowercaseUrls) { urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant(); } if (_options.LowercaseUrls && _options.LowercaseQueryStrings) { queryString = queryString.ToLowerInvariant(); } if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith('/')) { urlWithoutQueryString += "/"; } // queryString will contain the delimiter ? or # as the first character, so it's safe to append. url = urlWithoutQueryString + queryString; return new VirtualPathData(pathData.Router, url, pathData.DataTokens); } return pathData; } [MemberNotNull(nameof(_options))] private void EnsureOptions(HttpContext context) { if (_options == null) { _options = context.RequestServices.GetRequiredService<IOptions<RouteOptions>>().Value; } } }