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