开发者

How to make ASP.NET Routing escape route values?

I have an ASP.NET MVC site where I want routes like /{controller}/{id}/{action}/{date}, where "date" is the mm/dd/yyyy portion of a date/time. (I'm dealing with time-dimensioned data, so I need both an ID and a point in time to do most operations)

The route for this is 开发者_如何学JAVAsimple:

routes.MapRoute(
    "TimeDimensionedRoute",
    "{controller}/{id}/{action}/{date}",
    new { controller = "Iteration", action = "Index", id = String.Empty, date = String.Empty }
);

This route correctly maps "/Foo/100/Edit/01%2F21%2F2010" to the desired action. Update: this is incorrect. This is NOT routed correctly, I was mistaken. See the related question linked in the accepted answer.

My problem is that when I use Html.ActionLink() to generate a link for this route, it does not URL-encode the date and I end up with invalid URLs such as "/Foo/100/Edit/01/21/2010".

Is there any way to get the routing infrastructure to encode the values for me? It seems wrong that I have to manually URL-encode data that I pass to the HTML helpers.


I'm guessing that it doesn't automatically url encode it b/c its hard for the html helper to determine if you want to represent a date or if you want to have 3 more fields in the route e.g.

// Here's what you're seeing
/Foo  /100  /Edit  /10/21/2010/
// 4 route values

// But there's know way to know you don't want this
/Foo  /100  /Edit  /10  /21  /2010/
// 6 route values

Maybe you could change your route to be

...
"{controller}/{id}/{action}/{month}/{day}/{year}",
...

That way, it would always work without escaping.

Otherwise, you could do a URL Encoding of the date within the Html.ActionLink(...) call


I don't know if this counts as an answer or not, but I always use yyyy-mm-dd format in URIs. Not because slashes are reserved per the RFC (although that's a good reason) but because it is immune to globalization issues when converting to/from a string. DateTime.Parse() "just works" with this format, even if someone sets the server locale to somewhere in Eastern Europe.


I had the same problem because client codes could include / : and all sorts of characters. This is how I solved it: http://blog.peterlesliemorris.com/archive/2010/11/19/asp-mvc-encoding-route-values.aspx

This is what you need to do in your web app.

//1: Register a custom value provider in global.asax.cs
protected void Application_Start()
{
  EncodedRouteValueProviderFactory.Register();
  ...
}

//2: Use the following code in your views instead of Html.ActionLink
//this will ensure that all values before the ? query string part of your
//URL are properly encoded

<%: Html.EncodedActionLink(.....) %>
//3: Use this special redirect action when redirecting from a method
return this.EncodedActionLink(.....);

And this is the extension source code

//EncodedActionLinkExtensions.cs
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Routing;

namespace System.Web.Mvc.Html
{
  public static class EncodedActionLinkExtensions
  {
    public static MvcHtmlString EncodedActionLink(this HtmlHelper htmlHelper, string linkText, string action)
    {
      return htmlHelper.EncodedActionLink(linkText, action, (object)null);
    }

    public static MvcHtmlString EncodedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controllerName)
    {
      return htmlHelper.EncodedActionLink(linkText, action, controllerName, (object)null);
    }

    public static MvcHtmlString EncodedActionLink(this HtmlHelper htmlHelper, string linkText, string action, object explicitRouteValues)
    {
      object routeValueObj;
      if (!htmlHelper.ViewContext.RequestContext.RouteData.Values.TryGetValue("controller", out routeValueObj))
        throw new InvalidOperationException("Could not determine controller");

      string controllerName = (string)routeValueObj;
      return htmlHelper.EncodedActionLink(linkText, action, controllerName, explicitRouteValues);
    }

    public static MvcHtmlString EncodedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controllerName, object explicitRouteValues)
    {
      return htmlHelper.EncodedActionLink(linkText, action, controllerName, new RouteValueDictionary(explicitRouteValues));
    }

    public static MvcHtmlString EncodedActionLink(this HtmlHelper htmlHelper, string linkText, string action, string controllerName, RouteValueDictionary explicitRouteValues)
    {
      string url = EncodedUrlHelper.GenerateUrl(
        htmlHelper.ViewContext.RequestContext,
        controllerName, action, explicitRouteValues);
      string result = string.Format("<a href=\"{0}\">{1}</a>", url, linkText);
      return MvcHtmlString.Create(result);
    }
  }
}


//EncodedRedirectToRouteExtensions.cs
using System.Web.Routing;
namespace System.Web.Mvc
{
  public static class EncodedRedirectToRouteExtensions
  {
    public static EncodedRedirectToRouteResult EncodedRedirectToAction(this IController controller, string actionName)
    {
      return controller.EncodedRedirectToAction(
        actionName,
        (string)null, //controllerName,
        (RouteValueDictionary)null //routeValues
        );
    }

    public static EncodedRedirectToRouteResult EncodedRedirectToAction(this IController controller, string actionName, object routeValues)
    {
      return controller.EncodedRedirectToAction(
        actionName,
        (string)null, //controllerName,
        new RouteValueDictionary(routeValues)
        );
    }

    public static EncodedRedirectToRouteResult EncodedRedirectToAction(this IController controller, string actionName, RouteValueDictionary routeValues)
    {
      return controller.EncodedRedirectToAction(
        actionName,
        (string)null, //controllerName,
        routeValues
        );
    }

    public static EncodedRedirectToRouteResult EncodedRedirectToAction(this IController controller, string actionName, string controllerName)
    {
      return controller.EncodedRedirectToAction(
        actionName,
        controllerName,
        (RouteValueDictionary)null //routeValues
        );
    }

    public static EncodedRedirectToRouteResult EncodedRedirectToAction(this IController controller, string actionName, string controllerName, object routeValues)
    {
      return controller.EncodedRedirectToAction(
        actionName,
        controllerName,
        new RouteValueDictionary(routeValues)
        );
    }

    public static EncodedRedirectToRouteResult EncodedRedirectToAction(this IController controller, string actionName, string controllerName, RouteValueDictionary routeValues)
    {
      RouteValueDictionary dictionary;
      if (routeValues != null)
        dictionary = new RouteValueDictionary(routeValues);
      else
        dictionary = new RouteValueDictionary();
      dictionary["controller"] = controllerName;
      dictionary["action"] = actionName;

      var result = new EncodedRedirectToRouteResult(dictionary);
      return result;
    }

  }
}

//EncodedRedirectToRouteResult.cs
using System.Web.Mvc;
using System.Web.Routing;
namespace System.Web.Mvc
{
  public class EncodedRedirectToRouteResult : ActionResult
  {
    readonly string RouteName;
    readonly RouteValueDictionary RouteValues;

    public EncodedRedirectToRouteResult(RouteValueDictionary routeValues)
      : this(null, routeValues)
    {
    }

    public EncodedRedirectToRouteResult(string routeName, RouteValueDictionary routeValues)
    {
      RouteName = routeName ?? "";
      RouteValues = routeValues != null ? routeValues : new RouteValueDictionary();
    }

    public override void ExecuteResult(ControllerContext context)
    {
      string url = EncodedUrlHelper.GenerateUrl(context.RequestContext, null, null, RouteValues);
      context.Controller.TempData.Keep();
      context.HttpContext.Response.Redirect(url, false);
    }
  }
}

//EncodedRouteValueProvider.cs
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web.Routing;
using System.Reflection;
namespace System.Web.Mvc
{
  public class EncodedRouteValueProvider : IValueProvider
  {
    readonly ControllerContext ControllerContext;
    bool Activated = false;

    public EncodedRouteValueProvider(ControllerContext controllerContext)
    {
      ControllerContext = controllerContext;
    }

    public bool ContainsPrefix(string prefix)
    {
      if (!Activated)
        DecodeRouteValues();
      return false;
    }

    public ValueProviderResult GetValue(string key)
    {
      if (!Activated)
        DecodeRouteValues();
      return null;
    }

    void DecodeRouteValues()
    {
      Activated = true;
      var route = (Route)ControllerContext.RouteData.Route;
      string url = route.Url;
      var keysToDecode = new HashSet<string>();
      var regex = new Regex(@"\{.+?\}");
      foreach (Match match in regex.Matches(url))
        keysToDecode.Add(match.Value.Substring(1, match.Value.Length - 2));
      foreach (string key in keysToDecode)
      {
        object valueObj = ControllerContext.RequestContext.RouteData.Values[key];
        if (valueObj == null)
          continue;
        string value = valueObj.ToString();
        value = UrlValueEncoderDecoder.DecodeString(value);
        ControllerContext.RouteData.Values[key] = value;
        ValueProviderResult valueProviderResult = ControllerContext.Controller.ValueProvider.GetValue(key);
        if (valueProviderResult == null)
          continue;
        PropertyInfo attemptedValueProperty = valueProviderResult.GetType().GetProperty("AttemptedValue");
        attemptedValueProperty.SetValue(valueProviderResult, value, null);
        PropertyInfo rawValueProperty = valueProviderResult.GetType().GetProperty("RawValue");
        rawValueProperty.SetValue(valueProviderResult, value, null);
      }
    }

  }
}

//EncodedRouteValueProviderFactory.cs
namespace System.Web.Mvc
{
  public class EncodedRouteValueProviderFactory : ValueProviderFactory
  {
    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
      return new EncodedRouteValueProvider(controllerContext);
    }

    public static void Register()
    {
      ValueProviderFactories.Factories.Insert(0, new EncodedRouteValueProviderFactory());
    }
  }
}

//EncodedUrlHelper.cs
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Mvc;
namespace System.Web.Routing
{
  public static class EncodedUrlHelper
  {
    public static string GenerateUrl(
      RequestContext requestContext, 
      string controllerName,
      string action,
      RouteValueDictionary explicitRouteValues)
    {
      if (requestContext == null)
        throw new ArgumentNullException("RequestContext");

      var newRouteValues = RouteHelper.GetRouteValueDictionary(
        requestContext, controllerName, action, explicitRouteValues);
      var route = RouteHelper.GetRoute(requestContext, controllerName, action, newRouteValues);
      string url = route.Url;
      //Replace the {values} in the main part of the URL with request values
      var regex = new Regex(@"\{.+?\}");
      url = regex.Replace(url,
        match =>
        {
          string key = match.Value.Substring(1, match.Value.Length - 2);
          object value;
          if (!newRouteValues.TryGetValue(key, out value))
            throw new ArgumentNullException("Cannot reconcile value for key: " + key);
          string replaceWith;
          if (value == UrlParameter.Optional)
            replaceWith = "";
          else
            replaceWith = UrlValueEncoderDecoder.EncodeObject(value);
          explicitRouteValues.Remove(key);
          return replaceWith;
        });

      //2: Add additional values after the ?
      explicitRouteValues.Remove("controller");
      explicitRouteValues.Remove("action");
      var urlBuilder = new StringBuilder();
      urlBuilder.Append("/" + url);
      string separator = "?";
      foreach (var kvp in explicitRouteValues)
      {
        if (kvp.Value != UrlParameter.Optional)
        {
          urlBuilder.AppendFormat("{0}{1}={2}", separator, kvp.Key, kvp.Value == null ? "" : HttpUtility.UrlEncode(kvp.Value.ToString()));
          separator = "&";
        }
      }
      return urlBuilder.ToString();
    }
  }
}

//RouteHelper.cs
namespace System.Web.Routing
{
  public static class RouteHelper
  {
    public static RouteValueDictionary GetRouteValueDictionary(
      RequestContext requestContext,
      string controllerName,
      string action,
      RouteValueDictionary explicitRouteValues)
    {
      var newRouteValues = new RouteValueDictionary();
      var route = GetRoute(requestContext, controllerName, action, explicitRouteValues);
      MergeValues(route.Defaults, newRouteValues);
      MergeValues(requestContext.RouteData.Values, newRouteValues);
      if (explicitRouteValues != null)
        MergeValues(explicitRouteValues, newRouteValues);
      if (controllerName != null)
        newRouteValues["controller"] = controllerName;
      if (action != null)
        newRouteValues["action"] = action;
      return newRouteValues;
    }

    public static Route GetRoute(
      RequestContext requestContext,
      string controllerName,
      string action,
      RouteValueDictionary explicitRouteValues
      )
    {
      var routeValues = new RouteValueDictionary(requestContext.RouteData.Values);
      if (explicitRouteValues != null)
        MergeValues(explicitRouteValues, routeValues);
      if (controllerName != null)
        routeValues["controller"] = controllerName;
      if (action != null)
        routeValues["action"] = action;
      var virtualPath = RouteTable.Routes.GetVirtualPath(requestContext, routeValues);
      return (Route)virtualPath.Route;
    }

    static void MergeValues(RouteValueDictionary routeValues, RouteValueDictionary result)
    {
      foreach (var kvp in routeValues)
      {
        if (kvp.Value != null)
          result[kvp.Key] = kvp.Value;
        else
        {
          object value;
          if (!result.TryGetValue(kvp.Key, out value))
            result[kvp.Key] = null;
        }
      }
    }
  }
}

//UrlValueEncoderDecoder.cs
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace System.Web.Mvc
{
  public static class UrlValueEncoderDecoder
  {
    static HashSet<char> ValidChars;

    static UrlValueEncoderDecoder()
    {
      string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.";
      ValidChars = new HashSet<char>(chars.ToCharArray());
    }

    public static string EncodeObject(object value)
    {
      if (value == null)
        return null;
      return EncodeString(value.ToString());
    }

    public static string EncodeString(string value)
    {
      if (value == null)
        return null;
      var resultBuilder = new StringBuilder();
      foreach (char currentChar in value.ToCharArray())
        if (ValidChars.Contains(currentChar))
          resultBuilder.Append(currentChar);
        else
        {
          byte[] bytes = System.Text.UnicodeEncoding.UTF8.GetBytes(currentChar.ToString());
          foreach (byte currentByte in bytes)
            resultBuilder.AppendFormat("${0:x2}", currentByte);
        }
      string result = resultBuilder.ToString();
      //Special case, use + for spaces as it is shorter and spaces are common
      return result.Replace("$20", "+");
    }

    public static string DecodeString(string value)
    {
      if (value == null)
        return value;
      //Special case, change + back to a space
      value = value.Replace("+", " ");
      var regex = new Regex(@"\$[0-9a-fA-F]{2}");
      value = regex.Replace(value,
        match =>
        {
          string hexCode = match.Value.Substring(1, 2);
          byte byteValue = byte.Parse(hexCode, NumberStyles.AllowHexSpecifier);
          string decodedChar = System.Text.UnicodeEncoding.UTF8.GetString(new byte[] { byteValue });
          return decodedChar;
        });
      return value;
    }
  }
}


You can not use a forward slash in a route value in ASP.NET MVC. Even if it is URL-encoded it wont work.

slash in url

There is only a solution if you use ASP.NET 4.0

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜