clase Asp.net MVC ruta que los apoyos cajón de sastre en cualquier parámetro en la URL

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

  •  24-09-2019
  •  | 
  •  

Pregunta

cuanto más pienso en ello, más creo que es posible escribir una ruta personalizada que consumiría estas definiciones URL:

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

así como tener como máximo un parámetro codicioso en cualquier posición dentro de ninguna de las URL superiores como

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

Hay un limitación importante . Rutas con segmento codicioso no pueden tener ninguna pieza opcionales. Todos ellos son obligatorio .

Ejemplo

Esta es una solicitud ejemplar

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

Esta es la definición de la ruta URL

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

Algoritmo

Solicitud de análisis recorrido dentro GetRouteData() debería funcionar así:

  1. solicitud de Split en segmentos:
    • Mostrar
    • temas
    • SubTema
    • SubSubTopic
    • 123
    • Esto-es-an-ejemplo
  2. Definición de proceso URL ruta a partir de la izquierda y la asignación de valores de segmento individuales a parámetros (o valores de segmento de solicitud de juego para los segmentos de ruta constantes estáticas).
  3. Cuando segmento de ruta se define como el análisis sintáctico codicioso, inversa e ir al último segmento.
  4. segmentos de ruta Parse uno por uno hacia atrás (asignándoles solicitan valores) hasta llegar al cajón de sastre codiciosos uno nuevo.
  5. Cuando llegue al codicioso de nuevo, se unen a todos los segmentos restantes de solicitud (en orden original) y asignarlos al cajón de sastre parámetro de ruta codicioso.

Preguntas

Por lo que yo puedo pensar en esto, podría funcionar. Pero me gustaría saber:

  1. ¿Alguien ha escrito esto, así que no tengo que (debido a que hay otros aspectos a analizar, así que no he mencionado (restricciones, valores predeterminados, etc.)
  2. ¿existen otras fallas en este algoritmo, porque voy a tener que escribir yo mismo si nadie lo ha hecho hasta ahora.

No he pensado método GetVirtuaPath() en absoluto.

¿Fue útil?

Solución

Últimamente estoy haciendo preguntas en urgence, por lo que suelen resolver los problemas por mi cuenta. Lo siento por eso, pero aquí está mi opinión sobre el tipo de ruta que estaba preguntando. Alguien encuentra algún problema con él:. Hágamelo saber

Ruta con cajón de sastre en cualquier lugar en el segmento de 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
}

Y dos clases adicionales that're utilizan en el código superior:

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

y

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

Eso es todo amigos. Avisadme de cualquier problema.

También he escrito un entrada de blog relacionado con esta clase de ruta personalizada. En él se explica todo en gran detalle.

Otros consejos

Bueno. No puede ser de jerarquía predeterminada. Porque, la capa de enrutamiento de las acciones dividido. No se puede manipular enlaces de parámetros. Tienes que escribir nueva ActionInvoker o tienen que utilizar expresiones regulares para la captura.

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

controlador:

    public class HomeController : Controller
    {
        public ActionResult Index(string topics, int id, string title)
        {
            string[] arr = topics.Split('/')
        }
    }
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top