فئة مسار ASP.NET MVC التي تدعم المعلمة Catch-All في أي مكان في عنوان URL

StackOverflow https://stackoverflow.com/questions/2378222

  •  24-09-2019
  •  | 
  •  

سؤال

كلما فكرت في الأمر أكثر ، أعتقد أنه من الممكن كتابة مسار مخصص يستهلك تعريفات 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. تقسيم الطلب إلى شرائح:
    • يعرض
    • عنوان
    • تحت النظام
    • subsubtopic
    • 123
    • هذا مثال
  2. معالجة تعريف عنوان URL للمسار بدءًا من اليسار وتعيين قيم شريحة واحدة للمعلمات (أو مطابقة قيم شريحة الطلب إلى شرائح ثابتة ثابتة).
  3. عندما يتم تعريف مقطع الطريق على أنه جشع ، فإن التحليل العكسي وانتقل إلى الجزء الأخير.
  4. Parse Route Screps One تلو الأخرى (تعيين قيم طلبها) حتى تصل إلى الجشع مرة أخرى.
  5. عندما تصل إلى واحدة الجشع مرة أخرى ، انضم إلى جميع قطاعات الطلب المتبقية (بالترتيب الأصلي) وقم بتعيينها إلى معلمة المسار الجشع.

أسئلة

بقدر ما أستطيع التفكير في هذا ، يمكن أن ينجح. لكني أود أن أعرف:

  1. هل كتب أي شخص هذا بالفعل ، لذا لست مضطرًا إلى ذلك (لأن هناك جوانب أخرى لتحليلها أيضًا لم أذكرها (القيود ، الافتراضية وما إلى ذلك)
  2. هل ترى أي عيوب في هذه الخوارزمية ، لأنني سأضطر إلى كتابتها بنفسي إذا لم يفعل ذلك أحد حتى الآن.

لم أفكر في GetVirtuaPath() الطريقة على الإطلاق.

هل كانت مفيدة؟

المحلول

في الآونة الأخيرة ، أطرح أسئلة في Urgence ، لذلك عادةً ما أحل المشكلات بمفردي. آسف لذلك ، ولكن هذا هو رأيي على نوع الطريق الذي كنت أسأل عنه. أي شخص يجد أي مشاكل معها: اسمحوا لي أن أعرف.

مسار مع شريحة الجذب في أي مكان في عنوان 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
}

وفئتان إضافيتان تستخدمان في الكود العلوي:

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

هذا كل ما لدي أيها الناس. اسمحوا لي أن أعرف أي قضايا.

لقد كتبت أيضًا مشاركة مدونة تتعلق بهذه فئة الطريق المخصصة. يشرح كل شيء بتفاصيل رائعة.

نصائح أخرى

نحن سوف. لا يمكن أن يكون في التسلسل الهرمي الافتراضي. السبب ، طبقة التوجيه المنقسمة من الإجراءات. لا يمكنك معالجة روابط المعلمة. عليك أن تكتب 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('/')
        }
    }
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top