Asp.net MVC Route class that supports catch-all parameter anywhere in the URL

后端 未结 2 2020
广开言路
广开言路 2021-02-01 10:47

the more I think about it the more I believe it\'s possible to write a custom route that would consume these URL definitions:

{var1}/{var2}/{var3}
Const/{var1}/{         


        
2条回答
  •  猫巷女王i
    2021-02-01 11:15

    Lately I'm asking questions in urgence, so I usually solve problems on my own. Sorry for that, but here's my take on the kind of route I was asking about. Anyone finds any problems with it: let me know.

    Route with catch-all segment anywhere in the URL

    /// 
    /// This route is used for cases where we want greedy route segments anywhere in the route URL definition
    /// 
    public class GreedyRoute : Route
    {
        #region Properties
    
        public new string Url { get; private set; }
    
        private LinkedList urlSegments = new LinkedList();
    
        private bool hasGreedySegment = false;
    
        public int MinRequiredSegments { get; private set; }
    
        #endregion
    
        #region Constructors
    
        /// 
        /// Initializes a new instance of the  class, using the specified URL pattern and handler class.
        /// 
        /// The URL pattern for the route.
        /// The object that processes requests for the route.
        public GreedyRoute(string url, IRouteHandler routeHandler)
            : this(url, null, null, null, routeHandler)
        {
        }
    
        /// 
        /// Initializes a new instance of the  class, using the specified URL pattern, handler class, and default parameter values.
        /// 
        /// The URL pattern for the route.
        /// The values to use if the URL does not contain all the parameters.
        /// The object that processes requests for the route.
        public GreedyRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
            : this(url, defaults, null, null, routeHandler)
        {
        }
    
        /// 
        /// Initializes a new instance of the  class, using the specified URL pattern, handler class, default parameter values, and constraints.
        /// 
        /// The URL pattern for the route.
        /// The values to use if the URL does not contain all the parameters.
        /// A regular expression that specifies valid values for a URL parameter.
        /// The object that processes requests for the route.
        public GreedyRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
            : this(url, defaults, constraints, null, routeHandler)
        {
        }
    
        /// 
        /// Initializes a new instance of the  class, using the specified URL pattern, handler class, default parameter values, constraints, and custom values.
        /// 
        /// The URL pattern for the route.
        /// The values to use if the URL does not contain all the parameters.
        /// A regular expression that specifies valid values for a URL parameter.
        /// Custom values that are passed to the route handler, but which are not used to determine whether the route matches a specific URL pattern. The route handler might need these values to process the request.
        /// The object that processes requests for the route.
        public GreedyRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
            : base(url.Replace("*", ""), defaults, constraints, dataTokens, routeHandler)
        {
            this.Defaults = defaults ?? new RouteValueDictionary();
            this.Constraints = constraints;
            this.DataTokens = dataTokens;
            this.RouteHandler = routeHandler;
            this.Url = url;
            this.MinRequiredSegments = 0;
    
            // URL must be defined
            if (string.IsNullOrEmpty(url))
            {
                throw new ArgumentException("Route URL must be defined.", "url");
            }
    
            // correct URL definition can have AT MOST ONE greedy segment
            if (url.Split('*').Length > 2)
            {
                throw new ArgumentException("Route URL can have at most one greedy segment, but not more.", "url");
            }
    
            Regex rx = new Regex(@"^(?{)?(?(isToken)(?\*?))(?[a-zA-Z0-9-_]+)(?(isToken)})$", RegexOptions.Compiled | RegexOptions.Singleline);
            foreach (string segment in url.Split('/'))
            {
                // segment must not be empty
                if (string.IsNullOrEmpty(segment))
                {
                    throw new ArgumentException("Route URL is invalid. Sequence \"//\" is not allowed.", "url");
                }
    
                if (rx.IsMatch(segment))
                {
                    Match m = rx.Match(segment);
                    GreedyRouteSegment s = new GreedyRouteSegment {
                        IsToken = m.Groups["isToken"].Value.Length.Equals(1),
                        IsGreedy = m.Groups["isGreedy"].Value.Length.Equals(1),
                        Name = m.Groups["name"].Value
                    };
                    this.urlSegments.AddLast(s);
                    this.hasGreedySegment |= s.IsGreedy;
    
                    continue;
                }
                throw new ArgumentException("Route URL is invalid.", "url");
            }
    
            // get minimum required segments for this route
            LinkedListNode seg = this.urlSegments.Last;
            int sIndex = this.urlSegments.Count;
            while(seg != null && this.MinRequiredSegments.Equals(0))
            {
                if (!seg.Value.IsToken || !this.Defaults.ContainsKey(seg.Value.Name))
                {
                    this.MinRequiredSegments = Math.Max(this.MinRequiredSegments, sIndex);
                }
                sIndex--;
                seg = seg.Previous;
            }
    
            // check that segments after greedy segment don't define a default
            if (this.hasGreedySegment)
            {
                LinkedListNode s = this.urlSegments.Last;
                while (s != null && !s.Value.IsGreedy)
                {
                    if (s.Value.IsToken && this.Defaults.ContainsKey(s.Value.Name))
                    {
                        throw new ArgumentException(string.Format("Defaults for route segment \"{0}\" is not allowed, because it's specified after greedy catch-all segment.", s.Value.Name), "defaults");
                    }
                    s = s.Previous;
                }
            }
        }
    
        #endregion
    
        #region GetRouteData
        /// 
        /// Returns information about the requested route.
        /// 
        /// An object that encapsulates information about the HTTP request.
        /// 
        /// An object that contains the values from the route definition.
        /// 
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
    
            RouteValueDictionary values = this.ParseRoute(virtualPath);
            if (values == null)
            {
                return null;
            }
    
            RouteData result = new RouteData(this, this.RouteHandler);
            if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest))
            {
                return null;
            }
    
            // everything's fine, fill route data
            foreach (KeyValuePair value in values)
            {
                result.Values.Add(value.Key, value.Value);
            }
            if (this.DataTokens != null)
            {
                foreach (KeyValuePair token in this.DataTokens)
                {
                    result.DataTokens.Add(token.Key, token.Value);
                }
            }
            return result;
        }
        #endregion
    
        #region GetVirtualPath
        /// 
        /// Returns information about the URL that is associated with the route.
        /// 
        /// An object that encapsulates information about the requested route.
        /// An object that contains the parameters for a route.
        /// 
        /// An object that contains information about the URL that is associated with the route.
        /// 
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            RouteUrl url = this.Bind(requestContext.RouteData.Values, values);
            if (url == null)
            {
                return null;
            }
            if (!this.ProcessConstraints(requestContext.HttpContext, url.Values, RouteDirection.UrlGeneration))
            {
                return null;
            }
    
            VirtualPathData data = new VirtualPathData(this, url.Url);
            if (this.DataTokens != null)
            {
                foreach (KeyValuePair pair in this.DataTokens)
                {
                    data.DataTokens[pair.Key] = pair.Value;
                }
            }
            return data;
        }
        #endregion
    
        #region Private methods
    
        #region ProcessConstraints
        /// 
        /// Processes constraints.
        /// 
        /// The HTTP context.
        /// Route values.
        /// Route direction.
        /// true if constraints are satisfied; otherwise, false.
        private bool ProcessConstraints(HttpContextBase httpContext, RouteValueDictionary values, RouteDirection direction)
        {
            if (this.Constraints != null)
            {
                foreach (KeyValuePair constraint in this.Constraints)
                {
                    if (!this.ProcessConstraint(httpContext, constraint.Value, constraint.Key, values, direction))
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        #endregion
    
        #region ParseRoute
        /// 
        /// Parses the route into segment data as defined by this route.
        /// 
        /// Virtual path.
        /// Returns  dictionary of route values.
        private RouteValueDictionary ParseRoute(string virtualPath)
        {
            Stack parts = new Stack(virtualPath.Split(new char[] {'/'}, StringSplitOptions.RemoveEmptyEntries));
    
            // number of request route parts must match route URL definition
            if (parts.Count < this.MinRequiredSegments)
            {
                return null;
            }
    
            RouteValueDictionary result = new RouteValueDictionary();
    
            // start parsing from the beginning
            bool finished = false;
            LinkedListNode currentSegment = this.urlSegments.First;
            while (!finished && !currentSegment.Value.IsGreedy)
            {
                object p = parts.Pop();
                if (currentSegment.Value.IsToken)
                {
                    p = p ?? this.Defaults[currentSegment.Value.Name];
                    result.Add(currentSegment.Value.Name, p);
                    currentSegment = currentSegment.Next;
                    finished = currentSegment == null;
                    continue;
                }
                if (!currentSegment.Value.Equals(p))
                {
                    return null;
                }
            }
    
            // continue from the end if needed
            parts = new Stack(parts.Reverse());
            currentSegment = this.urlSegments.Last;
            while (!finished && !currentSegment.Value.IsGreedy)
            {
                object p = parts.Pop();
                if (currentSegment.Value.IsToken)
                {
                    p = p ?? this.Defaults[currentSegment.Value.Name];
                    result.Add(currentSegment.Value.Name, p);
                    currentSegment = currentSegment.Previous;
                    finished = currentSegment == null;
                    continue;
                }
                if (!currentSegment.Value.Equals(p))
                {
                    return null;
                }
            }
    
            // fill in the greedy catch-all segment
            if (!finished)
            {
                object remaining = string.Join("/", parts.Reverse().ToArray()) ?? this.Defaults[currentSegment.Value.Name];
                result.Add(currentSegment.Value.Name, remaining);
            }
    
            // add remaining default values
            foreach (KeyValuePair def in this.Defaults)
            {
                if (!result.ContainsKey(def.Key))
                {
                    result.Add(def.Key, def.Value);
                }
            }
    
            return result;
        }
        #endregion
    
        #region Bind
        /// 
        /// Binds the specified current values and values into a URL.
        /// 
        /// Current route data values.
        /// Additional route values that can be used to generate the URL.
        /// Returns a URL route string.
        private RouteUrl Bind(RouteValueDictionary currentValues, RouteValueDictionary values)
        {
            currentValues = currentValues ?? new RouteValueDictionary();
            values = values ?? new RouteValueDictionary();
    
            HashSet required = new HashSet(this.urlSegments.Where(seg => seg.IsToken).ToList().ConvertAll(seg => seg.Name), StringComparer.OrdinalIgnoreCase);
            RouteValueDictionary routeValues = new RouteValueDictionary();
    
            object dataValue = null;
            foreach (string token in new List(required))
            {
                dataValue = values[token] ?? currentValues[token] ?? this.Defaults[token];
                if (this.IsUsable(dataValue))
                {
                    string val = dataValue as string;
                    if (val != null)
                    {
                        val = val.StartsWith("/") ? val.Substring(1) : val;
                        val = val.EndsWith("/") ? val.Substring(0, val.Length - 1) : val;
                    }
                    routeValues.Add(token, val ?? dataValue);
                    required.Remove(token);
                }
            }
    
            // this route data is not related to this route
            if (required.Count > 0)
            {
                return null;
            }
    
            // add all remaining values
            foreach (KeyValuePair pair1 in values)
            {
                if (this.IsUsable(pair1.Value) && !routeValues.ContainsKey(pair1.Key))
                {
                    routeValues.Add(pair1.Key, pair1.Value);
                }
            }
    
            // add remaining defaults
            foreach (KeyValuePair pair2 in this.Defaults)
            {
                if (this.IsUsable(pair2.Value) && !routeValues.ContainsKey(pair2.Key))
                {
                    routeValues.Add(pair2.Key, pair2.Value);
                }
            }
    
            // check that non-segment defaults are the same as those provided
            RouteValueDictionary nonRouteDefaults = new RouteValueDictionary(this.Defaults);
            foreach (GreedyRouteSegment seg in this.urlSegments.Where(ss => ss.IsToken))
            {
                nonRouteDefaults.Remove(seg.Name);
            }
            foreach (KeyValuePair pair3 in nonRouteDefaults)
            {
                if (!routeValues.ContainsKey(pair3.Key) || !this.RoutePartsEqual(pair3.Value, routeValues[pair3.Key]))
                {
                    // route data is not related to this route
                    return null;
                }
            }
    
            StringBuilder sb = new StringBuilder();
            RouteValueDictionary valuesToUse = new RouteValueDictionary(routeValues);
            bool mustAdd = this.hasGreedySegment;
    
            // build URL string
            LinkedListNode s = this.urlSegments.Last;
            object segmentValue = null;
            while (s != null)
            {
                if (s.Value.IsToken)
                {
                    segmentValue = valuesToUse[s.Value.Name];
                    mustAdd = mustAdd || !this.RoutePartsEqual(segmentValue, this.Defaults[s.Value.Name]);
                    valuesToUse.Remove(s.Value.Name);
                }
                else
                {
                    segmentValue = s.Value.Name;
                    mustAdd = true;
                }
    
                if (mustAdd)
                {
                    sb.Insert(0, sb.Length > 0 ? "/" : string.Empty);
                    sb.Insert(0, Uri.EscapeUriString(Convert.ToString(segmentValue, CultureInfo.InvariantCulture)));
                }
    
                s = s.Previous;
            }
    
            // add remaining values
            if (valuesToUse.Count > 0)
            {
                bool first = true;
                foreach (KeyValuePair pair3 in valuesToUse)
                {
                    // only add when different from defaults
                    if (!this.RoutePartsEqual(pair3.Value, this.Defaults[pair3.Key]))
                    {
                        sb.Append(first ? "?" : "&");
                        sb.Append(Uri.EscapeDataString(pair3.Key));
                        sb.Append("=");
                        sb.Append(Uri.EscapeDataString(Convert.ToString(pair3.Value, CultureInfo.InvariantCulture)));
                        first = false;
                    }
                }
            }
    
            return new RouteUrl {
                Url = sb.ToString(),
                Values = routeValues
            };
        }
        #endregion
    
        #region IsUsable
        /// 
        /// Determines whether an object actually is instantiated or has a value.
        /// 
        /// Object value to check.
        /// 
        ///     true if an object is instantiated or has a value; otherwise, false.
        /// 
        private bool IsUsable(object value)
        {
            string val = value as string;
            if (val != null)
            {
                return val.Length > 0;
            }
            return value != null;
        }
        #endregion
    
        #region RoutePartsEqual
        /// 
        /// Checks if two route parts are equal
        /// 
        /// The first value.
        /// The second value.
        /// true if both values are equal; otherwise, false.
        private bool RoutePartsEqual(object firstValue, object secondValue)
        {
            string sFirst = firstValue as string;
            string sSecond = secondValue as string;
            if ((sFirst != null) && (sSecond != null))
            {
                return string.Equals(sFirst, sSecond, StringComparison.OrdinalIgnoreCase);
            }
            if ((sFirst != null) && (sSecond != null))
            {
                return sFirst.Equals(sSecond);
            }
            return (sFirst == sSecond);
        }
        #endregion
    
        #endregion
    }
    

    And additional two classes that're used within upper code:

    /// 
    /// Represents a route segment
    /// 
    public class RouteSegment
    {
        /// 
        /// Gets or sets segment path or token name.
        /// 
        /// Route segment path or token name.
        public string Name { get; set; }
    
        /// 
        /// Gets or sets a value indicating whether this segment is greedy.
        /// 
        /// true if this segment is greedy; otherwise, false.
        public bool IsGreedy { get; set; }
    
        /// 
        /// Gets or sets a value indicating whether this segment is a token.
        /// 
        /// true if this segment is a token; otherwise, false.
        public bool IsToken { get; set; }
    }
    

    and

    /// 
    /// Represents a generated route url with route data
    /// 
    public class RouteUrl
    {
        /// 
        /// Gets or sets the route URL.
        /// 
        /// Route URL.
        public string Url { get; set; }
    
        /// 
        /// Gets or sets route values.
        /// 
        /// Route values.
        public RouteValueDictionary Values { get; set; }
    }
    

    That's all folks. Let me know of any issues.

    I've also written a blog post related to this custom route class. It explains everything into great detail.

提交回复
热议问题