File: Routing\RouteCollection.cs
Project: ndp\fx\src\xsp\system\Web\System.Web.csproj (System.Web)
namespace System.Web.Routing {
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Runtime.CompilerServices;
    using System.Threading;
    using System.Web.Hosting;
 
    [TypeForwardedFrom("System.Web.Routing, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")]
    public class RouteCollection : Collection<RouteBase> {
        private Dictionary<string, RouteBase> _namedMap = new Dictionary<string, RouteBase>(StringComparer.OrdinalIgnoreCase);
        private VirtualPathProvider _vpp;
 
        private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
 
        public RouteCollection() {
        }
 
        public RouteCollection(VirtualPathProvider virtualPathProvider) {
            VPP = virtualPathProvider;
        }
 
        public bool AppendTrailingSlash {
            get;
            set;
        }
 
        public bool LowercaseUrls {
            get;
            set;
        }
 
        public bool RouteExistingFiles {
            get;
            set;
        }
 
        private VirtualPathProvider VPP {
            get {
                if (_vpp == null) {
                    return HostingEnvironment.VirtualPathProvider;
                }
                return _vpp;
            }
            set {
                _vpp = value;
            }
        }
 
        public RouteBase this[string name] {
            get {
                if (String.IsNullOrEmpty(name)) {
                    return null;
                }
                RouteBase route;
                if (_namedMap.TryGetValue(name, out route)) {
                    return route;
                }
                return null;
            }
        }
 
        public void Add(string name, RouteBase item) {
            if (item == null) {
                throw new ArgumentNullException("item");
            }
 
            if (!String.IsNullOrEmpty(name)) {
                if (_namedMap.ContainsKey(name)) {
                    throw new ArgumentException(
                        String.Format(
                            CultureInfo.CurrentUICulture,
                            SR.GetString(SR.RouteCollection_DuplicateName),
                            name),
                        "name");
                }
            }
 
            Add(item);
            if (!String.IsNullOrEmpty(name)) {
                _namedMap[name] = item;
            }
 
            // RouteBase doesn't have handler info, so we only log Route.RouteHandler
            var route = item as Route;
            if (route != null && route.RouteHandler != null) {
                TelemetryLogger.LogHttpHandler(route.RouteHandler.GetType());
            }            
        }
 
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
            Justification = "Warning was suppressed for consistency with existing similar routing API")]
        public Route MapPageRoute(string routeName, string routeUrl, string physicalFile) {
            return MapPageRoute(routeName, routeUrl, physicalFile, true /* checkPhysicalUrlAccess */, null, null, null);
        }
 
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
            Justification = "Warning was suppressed for consistency with existing similar routing API")]
        public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess) {
            return MapPageRoute(routeName, routeUrl, physicalFile, checkPhysicalUrlAccess, null, null, null);
        }
 
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
            Justification = "Warning was suppressed for consistency with existing similar routing API")]
        public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults) {
            return MapPageRoute(routeName, routeUrl, physicalFile, checkPhysicalUrlAccess, defaults, null, null);
        }
 
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
            Justification = "Warning was suppressed for consistency with existing similar routing API")]
        public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults, RouteValueDictionary constraints) {
            return MapPageRoute(routeName, routeUrl, physicalFile, checkPhysicalUrlAccess, defaults, constraints, null);
        }
 
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings",
            Justification = "Warning was suppressed for consistency with existing similar routing API")]
        public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens) {
            if (routeUrl == null) {
                throw new ArgumentNullException("routeUrl");
            }
            Route route = new Route(routeUrl, defaults, constraints, dataTokens, new PageRouteHandler(physicalFile, checkPhysicalUrlAccess));
            Add(routeName, route);
            return route;
        }
 
        [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
        protected override void ClearItems() {
            _namedMap.Clear();
            base.ClearItems();
        }
 
        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Not worth a breaking change.")]
        public IDisposable GetReadLock() {
            _rwLock.EnterReadLock();
            return new ReadLockDisposable(_rwLock);
        }
 
        private RequestContext GetRequestContext(RequestContext requestContext) {
            if (requestContext != null) {
                return requestContext;
            }
            HttpContext httpContext = HttpContext.Current;
            if (httpContext == null) {
                throw new InvalidOperationException(SR.GetString(SR.RouteCollection_RequiresContext));
            }
            return new RequestContext(new HttpContextWrapper(httpContext), new RouteData());
        }
 
        // Returns true if this is a request to an existing file
        private bool IsRouteToExistingFile(HttpContextBase httpContext) {
            string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
            return ((requestPath != "~/") &&
                (VPP != null) &&
                (VPP.FileExists(requestPath) ||
                VPP.DirectoryExists(requestPath)));
        }
 
        public RouteData GetRouteData(HttpContextBase httpContext) {
            if (httpContext == null) {
                throw new ArgumentNullException("httpContext");
            }
            if (httpContext.Request == null) {
                throw new ArgumentException(SR.GetString(SR.RouteTable_ContextMissingRequest), "httpContext");
            }
 
            // Optimize performance when the route collection is empty.  The main improvement is that we avoid taking
            // a read lock when the collection is empty.  Without this check, the UrlRoutingModule causes a 25%-50%
            // regression in HelloWorld RPS due to lock contention.  The UrlRoutingModule is now in the root web.config,
            // so we need to ensure the module is performant, especially when you are not using routing.
            // This check does introduce a slight bug, in that if a writer clears the collection as part of a write
            // transaction, a reader may see the collection when it's empty, which the read lock is supposed to prevent.
            // We will investigate a better fix in Dev10 Beta2.  The Beta1 bug is Dev10 652986.
            if (Count == 0) {
                return null;
            }
 
            bool isRouteToExistingFile = false;
            bool doneRouteCheck = false; // We only want to do the route check once
            if (!RouteExistingFiles) {
                isRouteToExistingFile = IsRouteToExistingFile(httpContext);
                doneRouteCheck = true;
                if (isRouteToExistingFile) {
                    // If we're not routing existing files and the file exists, we stop processing routes
                    return null;
                }
            }
 
            // Go through all the configured routes and find the first one that returns a match
            using (GetReadLock()) {
                foreach (RouteBase route in this) {
                    RouteData routeData = route.GetRouteData(httpContext);
                    if (routeData != null) {
                        // If we're not routing existing files on this route and the file exists, we also stop processing routes
                        if (!route.RouteExistingFiles) {
                            if (!doneRouteCheck) {
                                isRouteToExistingFile = IsRouteToExistingFile(httpContext);
                                doneRouteCheck = true;
                            }
                            if (isRouteToExistingFile) {
                                return null;
                            }
                        }
                        return routeData;
                    }
                }
            }
 
            return null;
        }
 
 
        [SuppressMessage("Microsoft.Globalization", "CA1307:SpecifyStringComparison", MessageId = "System.String.EndsWith(System.String)", Justification = @"okay")]
        private string NormalizeVirtualPath(RequestContext requestContext, string virtualPath) {
            string url = System.Web.UI.Util.GetUrlWithApplicationPath(requestContext.HttpContext, virtualPath);
 
            if (LowercaseUrls || AppendTrailingSlash) {
                int iqs = url.IndexOfAny(new char[] { '?', '#' });
                string urlWithoutQs;
                string qs;
                if (iqs >= 0) {
                    urlWithoutQs = url.Substring(0, iqs);
                    qs = url.Substring(iqs);
                }
                else {
                    urlWithoutQs = url;
                    qs = "";
                }
 
                // Don't lowercase the query string
                if (LowercaseUrls) {
                    urlWithoutQs = urlWithoutQs.ToLowerInvariant();
                }
 
                if (AppendTrailingSlash && !urlWithoutQs.EndsWith("/")) {
                    urlWithoutQs += "/";
                }
 
                url = urlWithoutQs + qs;
            }
 
            return url;
        }
 
        public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
            requestContext = GetRequestContext(requestContext);
 
            // Go through all the configured routes and find the first one that returns a match
            using (GetReadLock()) {
                foreach (RouteBase route in this) {
                    VirtualPathData vpd = route.GetVirtualPath(requestContext, values);
                    if (vpd != null) {
                        vpd.VirtualPath = NormalizeVirtualPath(requestContext, vpd.VirtualPath);
                        return vpd;
                    }
                }
            }
 
            return null;
        }
 
        public VirtualPathData GetVirtualPath(RequestContext requestContext, string name, RouteValueDictionary values) {
            requestContext = GetRequestContext(requestContext);
 
            if (!String.IsNullOrEmpty(name)) {
                RouteBase namedRoute;
                bool routeFound;
                using (GetReadLock()) {
                    routeFound = _namedMap.TryGetValue(name, out namedRoute);
                }
                if (routeFound) {
                    VirtualPathData vpd = namedRoute.GetVirtualPath(requestContext, values);
                    if (vpd != null) {
                        vpd.VirtualPath = NormalizeVirtualPath(requestContext, vpd.VirtualPath);
                        return vpd;
                    }
                    return null;
                }
                else {
                    throw new ArgumentException(
                        String.Format(
                            CultureInfo.CurrentUICulture,
                            SR.GetString(SR.RouteCollection_NameNotFound),
                            name),
                        "name");
                }
            }
            else {
                return GetVirtualPath(requestContext, values);
            }
        }
 
        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Not worth a breaking change.")]
        public IDisposable GetWriteLock() {
            _rwLock.EnterWriteLock();
            return new WriteLockDisposable(_rwLock);
        }
 
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", 
            Justification = "This is not a regular URL as it may contain special routing characters.")]
        public void Ignore(string url) {
            Ignore(url, null /* constraints */);
        }
 
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", 
            Justification = "This is not a regular URL as it may contain special routing characters.")]
        public void Ignore(string url, object constraints) {
            if (url == null) {
                throw new ArgumentNullException("url");
            }
 
            IgnoreRouteInternal route = new IgnoreRouteInternal(url) {
                Constraints = new RouteValueDictionary(constraints)
            };
 
            Add(route);
        }
 
        [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
        protected override void InsertItem(int index, RouteBase item) {
            if (item == null) {
                throw new ArgumentNullException("item");
            }
 
            if (Contains(item)) {
                throw new ArgumentException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        SR.GetString(SR.RouteCollection_DuplicateEntry)),
                    "item");
            }
            base.InsertItem(index, item);
        }
 
        [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
        protected override void RemoveItem(int index) {
            RemoveRouteName(index);
            base.RemoveItem(index);
        }
 
        private void RemoveRouteName(int index) {
            // Search for the specified route and clear out its name if we have one
            RouteBase route = this[index];
            foreach (KeyValuePair<string, RouteBase> namedRoute in _namedMap) {
                if (namedRoute.Value == route) {
                    _namedMap.Remove(namedRoute.Key);
                    break;
                }
            }
        }
 
        [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")]
        protected override void SetItem(int index, RouteBase item) {
            if (item == null) {
                throw new ArgumentNullException("item");
            }
 
            if (Contains(item)) {
                throw new ArgumentException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        SR.GetString(SR.RouteCollection_DuplicateEntry)),
                    "item");
            }
            RemoveRouteName(index);
            base.SetItem(index, item);
        }
 
        private class ReadLockDisposable : IDisposable {
 
            private ReaderWriterLockSlim _rwLock;
 
            public ReadLockDisposable(ReaderWriterLockSlim rwLock) {
                _rwLock = rwLock;
            }
 
            [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly",
                Justification = "Type does not have a finalizer.")]
            void IDisposable.Dispose() {
                _rwLock.ExitReadLock();
            }
        }
 
        private class WriteLockDisposable : IDisposable {
 
            private ReaderWriterLockSlim _rwLock;
 
            public WriteLockDisposable(ReaderWriterLockSlim rwLock) {
                _rwLock = rwLock;
            }
 
            [SuppressMessage("Microsoft.Usage", "CA1816:CallGCSuppressFinalizeCorrectly",
                Justification = "Type does not have a finalizer.")]
            void IDisposable.Dispose() {
                _rwLock.ExitWriteLock();
            }
        }
 
        private sealed class IgnoreRouteInternal : Route {
            public IgnoreRouteInternal(string url)
                : base(url, new StopRoutingHandler()) {
            }
 
            public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) {
                // Never match during route generation. This avoids the scenario where an IgnoreRoute with
                // fairly relaxed constraints ends up eagerly matching all generated URLs.
                return null;
            }
        }
    }
}