I have an ASP.NET MVC 3 application where users can post suggestions along the lines of "bla bla would be better if yada yada yada".
For the suggestion detail page I have defined a nice SEO friendly route as follows:
routes.MapRoute(null, "suggestion/{id}/{it}/would-be-better-if-{if}",
new { controller = "suggestion", action = "details" });
As you can see I want the "would be better if" part to be fixed.
This route works perfectly for any old suggestion and generates links like suggestion/5/this-site/would-be-better-if-it-had-a-iphone-application
, and clicking on the link actually requests the appropriate detail page.
A friend of mine, who ironically happens to be a tester, has managed to, involuntarily, post a suggestion that actually breaks the route: "This site would be better if 'would be better if' was always aligned in the middle".
The link generated for this suggestion is
/suggestion/84/this-site/would-be-better-if-would-be-better-if-was-always-alligned-in-the-middle
.
I have tried Phil Haack's Routing Debugger and have confirmed that the route will actually work up until suggestion/84/this-site/would-be-better-if-would-be-better-if-
, so the second "would-be-better-if" is actually accepted; adding anything after that will actually cause the url not to match any route (thanks to Omar -see comments- for help).
Please bear in mind that I really don't want to change the route definition since I think it is as good as I can manage for this case, SEO-wise.
So, how come having text equal to the fixed part of the route prevents the link from matching the route? why is the route breaking?
I am actually rahter more interested in the why, as I believe understanding the why will lead to a solution or at least a proper understanding of a rather interesting problem.
This looks like a dupe of ASP.NET routing: Literal sub-segment between tokens, and route values with a character from the literal sub-segment which is a much simpler version of the bug. I'd recommend closing this one in favor of that one.
I answered that question.
I'm not sure why it behaves this way, but you can use something like this:
public interface IRouteRule
{
object ProcessIncoming(object value);
object ProcessOutgoing(object value);
}
public class StartsWithRouteRule : IRouteRule
{
public StartsWithRouteRule(string value)
{
Value = value;
}
public string Value { get; protected set; }
public object ProcessIncoming(object value)
{
var result = value as string;
if (result == null)
return null;
if (!result.StartsWith(Value))
return null;
return result.Substring(Value.Length);
}
public object ProcessOutgoing(object value)
{
var result = value as string;
if (result == null)
return null;
return Value + result;
}
}
public class ComplexRoute : Route
{
public ComplexRoute(string url, object defaults, object rules)
: this(url, new RouteValueDictionary(defaults), rules)
{ }
public ComplexRoute(string url, RouteValueDictionary defaults, object rules)
: base(url, defaults, new MvcRouteHandler())
{
Rules = new Dictionary<string, IRouteRule>();
foreach (var pair in new RouteValueDictionary(rules))
Rules.Add(pair.Key, (IRouteRule)pair.Value);
}
public Dictionary<string, IRouteRule> Rules { get; protected set; }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var result = base.GetRouteData(httpContext);
if (result == null)
return null;
foreach (var pair in Rules)
{
var currentValue = result.Values[pair.Key];
if (currentValue == null)
return null;
var value = pair.Value.ProcessIncoming(currentValue);
if (value == null)
return null;
result.Values[pair.Key] = value;
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
values = new RouteValueDictionary(values);
foreach (var pair in Rules)
{
var currentValue = values[pair.Key];
if (currentValue == null)
return null;
var value = pair.Value.ProcessOutgoing(currentValue);
if (value == null)
return null;
values[pair.Key] = value;
}
return base.GetVirtualPath(requestContext, values);
}
}
Usage:
routes.Add(new ComplexRoute(
"suggestion/{id}/{it}/{if}",
new { controller = "suggestion", action = "details" },
new { @if = new StartsWithRouteRule("would-be-better-if-") }));