8

考えれば考えるほど、これらの URL 定義を使用するカスタム ルートを作成できる可能性があると思います。

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

同様に、任意の上位 URL 内の任意の位置に最大1 つの貪欲なパラメーターを含めることができます。

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

重要な制約が 1 つあります。貪欲なセグメントを持つルートには、オプションのパーツを含めることはできません。これらはすべて必須です。

これは典型的なリクエストです

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

これは URL ルート定義です

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

アルゴリズム

内部のリクエスト ルートの解析GetRouteData()は、次のように動作するはずです。

  1. リクエストをセグメントに分割します。
    • 見せる
    • トピック
    • サブトピック
    • サブサブトピック
    • 123
    • これは一例です
  2. 左から開始し、単一のセグメント値をパラメーターに割り当てて (または要求セグメント値を静的ルート定数セグメントに一致させて) ルート URL 定義を処理します。
  3. ルート セグメントがグリーディとして定義されている場合、解析を逆にして最後のセグメントに移動します。
  4. 貪欲なキャッチオールに再び到達するまで、ルート セグメントを 1 つずつ逆方向に解析します (要求値を割り当てます)。
  5. 貪欲なセグメントに再び到達したら、残りのすべてのリクエスト セグメントを (元の順序で) 結合し、それらを貪欲なキャッチオール ルート パラメーターに割り当てます。

質問

私が考える限り、これはうまくいく可能性があります。しかし、私は知りたいです:

  1. 誰かがすでにこれを書いているので、私はそうする必要はありません(解析には言及しなかった他の側面があるため(制約、デフォルトなど))
  2. このアルゴリズムに何か欠陥があると思いますか? 誰もそれをやったことがなければ、自分で書かなければならないからです。

方法は全く考えていませんGetVirtuaPath()

4

2 に答える 2

9

最近は急ぎの質問をするので、自分で問題を解決することが多いです。申し訳ありませんが、これが私が求めていた種類のルートに関する私の見解です。誰もがそれに問題を見つけました: 私に知らせてください.

URL の任意の場所にキャッチオール セグメントを含むルーティング

/// <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
}

上位コード内で使用される追加の 2 つのクラス:

/// <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; }
}

それはすべての人々です。問題があればお知らせください。

このカスタム ルート クラスに関連するブログ投稿も書いています。それはすべてを非常に詳細に説明します。

于 2010-03-04T17:06:24.300 に答える
5

良い。デフォルトの階層にあることはできません。なぜなら、ルーティング層がアクションから分離されているからです。パラメータ バインディングを操作することはできません。新しい ActionInvoker を作成するか、キャッチに RegEx を使用する必要があります。

Global.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('/')
        }
    }
于 2010-03-04T16:16:56.723 に答える