【问题标题】:Asp.net MVC Route class that supports catch-all parameter anywhere in the URLAsp.net MVC Route 类,支持 URL 中任意位置的 catch-all 参数
【发布时间】:2011-01-23 14:35:40
【问题描述】:

我想得越多,我就越相信可以编写一个自定义路由来使用这些 URL 定义:

{var1}/{var2}/{var3}
Const/{var1}/{var2}
Const1/{var1}/Const2/{var2}
{var1}/{var2}/Const

以及至多在任何上层 URL 中的任何位置上都有一个贪心参数,例如

{*var1}/{var2}/{var3}
{var1}/{*var2}/{var3}
{var1}/{var2}/{*var3}

有一个重要的约束。贪婪段的路由不能有任何可选部分。所有这些都是强制

示例

这是一个示例性请求

http://localhost/Show/Topic/SubTopic/SubSubTopic/123/This-is-an-example

这是 URL 路由定义

{action}/{*topicTree}/{id}/{title}

算法

GetRouteData() 中解析请求路由应该是这样的:

  1. 将请求拆分为多个段:
    • 显示
    • 主题
    • 子主题
    • 子子主题
    • 123
    • 这是一个例子
  2. 从左侧开始处理路由 URL 定义并将单个段值分配给参数(或将请求段值匹配到静态路由常量段)。
  3. 当路由段被定义为贪婪时,反向解析并转到最后一段。
  4. 向后逐一解析路由段(为其分配请求值),直到您再次到达贪婪的全包式。
  5. 当您再次到达贪婪节点时,加入所有剩余的请求段(按原始顺序)并将它们分配给贪婪的 catch-all 路由参数。

问题

据我所知,它可以工作。但我想知道:

  1. 有没有人写过这个,所以我不必写(因为还有其他我没有提到的解析方面(约束、默认值等)
  2. 您是否发现此算法存在任何缺陷,因为如果到目前为止没有人这样做,我将不得不自己编写它。

我根本没有想过GetVirtuaPath()方法。

【问题讨论】:

    标签: asp.net-mvc routing catch-all


    【解决方案1】:

    最近我在紧急提问,所以我通常自己解决问题。很抱歉,但这是我对我所询问的那种路线的看法。任何人发现它有任何问题:请告诉我。

    在 URL 中的任何位置使用 catch-all 段路由

    /// <summary>
    /// This route is used for cases where we want greedy route segments anywhere in the route URL definition
    /// </summary>
    public class GreedyRoute : Route
    {
        #region Properties
    
        public new string Url { get; private set; }
    
        private LinkedList<GreedyRouteSegment> urlSegments = new LinkedList<GreedyRouteSegment>();
    
        private bool hasGreedySegment = false;
    
        public int MinRequiredSegments { get; private set; }
    
        #endregion
    
        #region Constructors
    
        /// <summary>
        /// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern and handler class.
        /// </summary>
        /// <param name="url">The URL pattern for the route.</param>
        /// <param name="routeHandler">The object that processes requests for the route.</param>
        public GreedyRoute(string url, IRouteHandler routeHandler)
            : this(url, null, null, null, routeHandler)
        {
        }
    
        /// <summary>
        /// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern, handler class, and default parameter values.
        /// </summary>
        /// <param name="url">The URL pattern for the route.</param>
        /// <param name="defaults">The values to use if the URL does not contain all the parameters.</param>
        /// <param name="routeHandler">The object that processes requests for the route.</param>
        public GreedyRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
            : this(url, defaults, null, null, routeHandler)
        {
        }
    
        /// <summary>
        /// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern, handler class, default parameter values, and constraints.
        /// </summary>
        /// <param name="url">The URL pattern for the route.</param>
        /// <param name="defaults">The values to use if the URL does not contain all the parameters.</param>
        /// <param name="constraints">A regular expression that specifies valid values for a URL parameter.</param>
        /// <param name="routeHandler">The object that processes requests for the route.</param>
        public GreedyRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
            : this(url, defaults, constraints, null, routeHandler)
        {
        }
    
        /// <summary>
        /// Initializes a new instance of the <see cref="VivaRoute"/> class, using the specified URL pattern, handler class, default parameter values, constraints, and custom values.
        /// </summary>
        /// <param name="url">The URL pattern for the route.</param>
        /// <param name="defaults">The values to use if the URL does not contain all the parameters.</param>
        /// <param name="constraints">A regular expression that specifies valid values for a URL parameter.</param>
        /// <param name="dataTokens">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.</param>
        /// <param name="routeHandler">The object that processes requests for the route.</param>
        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>{)?(?(isToken)(?<isGreedy>\*?))(?<name>[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<GreedyRouteSegment> 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<GreedyRouteSegment> 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
        /// <summary>
        /// Returns information about the requested route.
        /// </summary>
        /// <param name="httpContext">An object that encapsulates information about the HTTP request.</param>
        /// <returns>
        /// An object that contains the values from the route definition.
        /// </returns>
        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<string, object> value in values)
            {
                result.Values.Add(value.Key, value.Value);
            }
            if (this.DataTokens != null)
            {
                foreach (KeyValuePair<string, object> token in this.DataTokens)
                {
                    result.DataTokens.Add(token.Key, token.Value);
                }
            }
            return result;
        }
        #endregion
    
        #region GetVirtualPath
        /// <summary>
        /// Returns information about the URL that is associated with the route.
        /// </summary>
        /// <param name="requestContext">An object that encapsulates information about the requested route.</param>
        /// <param name="values">An object that contains the parameters for a route.</param>
        /// <returns>
        /// An object that contains information about the URL that is associated with the route.
        /// </returns>
        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<string, object> pair in this.DataTokens)
                {
                    data.DataTokens[pair.Key] = pair.Value;
                }
            }
            return data;
        }
        #endregion
    
        #region Private methods
    
        #region ProcessConstraints
        /// <summary>
        /// Processes constraints.
        /// </summary>
        /// <param name="httpContext">The HTTP context.</param>
        /// <param name="values">Route values.</param>
        /// <param name="direction">Route direction.</param>
        /// <returns><c>true</c> if constraints are satisfied; otherwise, <c>false</c>.</returns>
        private bool ProcessConstraints(HttpContextBase httpContext, RouteValueDictionary values, RouteDirection direction)
        {
            if (this.Constraints != null)
            {
                foreach (KeyValuePair<string, object> constraint in this.Constraints)
                {
                    if (!this.ProcessConstraint(httpContext, constraint.Value, constraint.Key, values, direction))
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        #endregion
    
        #region ParseRoute
        /// <summary>
        /// Parses the route into segment data as defined by this route.
        /// </summary>
        /// <param name="virtualPath">Virtual path.</param>
        /// <returns>Returns <see cref="System.Web.Routing.RouteValueDictionary"/> dictionary of route values.</returns>
        private RouteValueDictionary ParseRoute(string virtualPath)
        {
            Stack<string> parts = new Stack<string>(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<GreedyRouteSegment> 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<string>(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<string, object> def in this.Defaults)
            {
                if (!result.ContainsKey(def.Key))
                {
                    result.Add(def.Key, def.Value);
                }
            }
    
            return result;
        }
        #endregion
    
        #region Bind
        /// <summary>
        /// Binds the specified current values and values into a URL.
        /// </summary>
        /// <param name="currentValues">Current route data values.</param>
        /// <param name="values">Additional route values that can be used to generate the URL.</param>
        /// <returns>Returns a URL route string.</returns>
        private RouteUrl Bind(RouteValueDictionary currentValues, RouteValueDictionary values)
        {
            currentValues = currentValues ?? new RouteValueDictionary();
            values = values ?? new RouteValueDictionary();
    
            HashSet<string> required = new HashSet<string>(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<string>(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<string, object> pair1 in values)
            {
                if (this.IsUsable(pair1.Value) && !routeValues.ContainsKey(pair1.Key))
                {
                    routeValues.Add(pair1.Key, pair1.Value);
                }
            }
    
            // add remaining defaults
            foreach (KeyValuePair<string, object> 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<string, object> 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<GreedyRouteSegment> 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<string, object> 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
        /// <summary>
        /// Determines whether an object actually is instantiated or has a value.
        /// </summary>
        /// <param name="value">Object value to check.</param>
        /// <returns>
        ///     <c>true</c> if an object is instantiated or has a value; otherwise, <c>false</c>.
        /// </returns>
        private bool IsUsable(object value)
        {
            string val = value as string;
            if (val != null)
            {
                return val.Length > 0;
            }
            return value != null;
        }
        #endregion
    
        #region RoutePartsEqual
        /// <summary>
        /// Checks if two route parts are equal
        /// </summary>
        /// <param name="firstValue">The first value.</param>
        /// <param name="secondValue">The second value.</param>
        /// <returns><c>true</c> if both values are equal; otherwise, <c>false</c>.</returns>
        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
    }
    

    还有另外两个在上面的代码中使用的类:

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

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

    这就是所有人。让我知道任何问题。

    我还写了一个与这个自定义路由类相关的blog post。它非常详细地解释了一切。

    【讨论】:

    • 我已经更新了GreedyRoute 类,因为其中有一个错误。主构造函数以及GetVirtualPath 方法已更改。如果您使用此代码,则必须对其进行更新。
    【解决方案2】:

    嗯。它不能在默认层次结构中。因为,路由层从动作中分离出来。您不能操纵参数绑定。您必须编写新的 ActionInvoker 或必须使用 RegEx 进行捕获。

    全球.asax:

    routes.Add(new RegexRoute("Show/(?<topics>.*)/(?<id>[\\d]+)/(?<title>.*)", 
        new { controller = "Home", action = "Index" }));
    
    public class RegexRoute : Route
    {
        private readonly Regex _regEx;
        private readonly RouteValueDictionary _defaultValues;
    
        public RegexRoute(string pattern, object defaultValues)
            : this(pattern, new RouteValueDictionary(defaultValues))
        { }
    
        public RegexRoute(string pattern, RouteValueDictionary defaultValues)
            : this(pattern, defaultValues, new MvcRouteHandler())
        { }
    
        public RegexRoute(string pattern, RouteValueDictionary defaultValues, 
            IRouteHandler routeHandler)
            : base(null, routeHandler)
        {
            this._regEx = new Regex(pattern);
            this._defaultValues = defaultValues;
        }
    
        private void AddDefaultValues(RouteData routeData)
        {
            if (this._defaultValues != null)
            {
                foreach (KeyValuePair<string, object> pair in this._defaultValues)
                {
                    routeData.Values[pair.Key] = pair.Value;
                }
            }
        }
    
        public override RouteData GetRouteData(System.Web.HttpContextBase httpContext)
        {
            string requestedUrl = 
                httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + 
                httpContext.Request.PathInfo;
            Match match = _regEx.Match(requestedUrl);
    
            if (match.Success)
            {
                RouteData routeData = new RouteData(this, this.RouteHandler);
                AddDefaultValues(routeData);
    
                for (int i = 0; i < match.Groups.Count; i++)
                {
                    string key = _regEx.GroupNameFromNumber(i);
                    Group group = match.Groups[i];
                    if (!string.IsNullOrEmpty(key))
                    {
                        routeData.Values[key] = group.Value;
                    }
                }
    
                return routeData;
            }
    
            return null;
        }
    }
    

    控制器:

        public class HomeController : Controller
        {
            public ActionResult Index(string topics, int id, string title)
            {
                string[] arr = topics.Split('/')
            }
        }
    

    【讨论】:

    • 这很聪明,有一个RegexRoute。我承认。但是您的路线只是一种方式,因为它没有 GetVirtualPath() 方法,这使得它无法与 Html 助手一起使用(ActionLinkBeginForm 等)
    • @cem:但我真的不知道您所说的 它是什么意思,它不能在默认层次结构中。因为,路由层从动作中分离出来。您不能操纵参数绑定。您必须编写新的 ActionInvoker 或必须使用 RegEx 进行捕获。
    • 最后但并非最不重要的一点是:我已经编写了自己的路线来完成所有的魔法。我可以在路由 URL 定义中的任何位置放置包罗万象的段。它是双向的。
    • 是的,是的,我写的时候没看清楚,因为我说了。我只想提一下,也许您想为动作数组和其他数组创建自动映射器,并建议使用新的调用程序或使用正则表达式。很抱歉,你是对的。
    猜你喜欢
    • 2012-03-05
    • 2011-02-11
    • 1970-01-01
    • 1970-01-01
    • 2014-06-26
    • 2023-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多