I've been experimenting with writing my own C# URL Rewriter using a HttpModule. It's surprisingly simple as .NET kindly has the following (where newURL is the path to the actual file) :
HttpContext.RewritePath(string newURL)
So this allows a URL such as:
/my-friendly-url
to actually point to:
/pages/content.aspx?id=5
Problem:
All was going well, but some pages were giving the following error for no apparent reason:
Cannot use a leading '..' to exit above the top directory.
After some investigation I tracked the problem down to any asp.net HyperLink controls that had an ImageUrl property set. More specifically those starting with a '~/', if I removed the tilde the problem went away. Suffice to say I wasn't entirely happy about this 'workaround' so set about looking for an answer to why it was happening, and a better solution.
Cause:
After what seemed like hours of googling I came across a great article (link at the end of this post) on 4guysfromRolla.com. The problem turns out to be a bug in the HyperLink control, whereby it 'double-resolves' the ImageUrl property (basically it doesn't check if the path has already been resolved further down the stack and calls base.ResolveClientUrl(imageUrl) every time).
Solution:
Thankfully the solution is impressively simple through the use of a .NET ControlAdapter. If you've not come across them before, ControlAdapters let you hook into (and/or alter) the rendering process of any control, without having to the change the implementation of it in every page (I don't want to have to change all my <asp:HyperLink /> definitions throughout my solution).
So to fix the problem i'm going to create a ControlAdapter (for all browsers - typically you'd use them for Browser specific rendering differences) that will override the HyperLink's render method, crucially not calling base.ResolveClientUrl(imageUrl), instead leaving the ImageUrl value 'as-is'. (see below)
public class HyperLinkControlAdapter : ControlAdapter
{
protected override void Render(HtmlTextWriter writer)
{
HyperLink hyperlink = this.Control as HyperLink;
if (hyperlink == null)
{
base.Render(writer);
return;
}
// This code is copied from HyperLink.RenderContents (using
// Reflector). References to "this" have been changed to
// "hyperlink", and we have to render the begin and end tags.
string imageUrl = hl.ImageUrl;
if (imageUrl.Length > 0)
{
// Let the HyperLink render its begin tag
hyperlink.RenderBeginTag(writer);
Image image = new Image();
// I think the next line is the bug. The URL gets
// resolved here, but the Image.UrlResolved property
// doesn't get set. So another attempt to resolve the
// URL is made in Image.AddAttributesToRender. It's in
// the callstack above that method that the exception
// or improperly resolved URL happens.
//image.ImageUrl = base.ResolveClientUrl(imageUrl);
image.ImageUrl = imageUrl;
imageUrl = hl.ToolTip;
if (imageUrl.Length != 0)
{
image.ToolTip = imageUrl;
}
imageUrl = hl.Text;
if (imageUrl.Length != 0)
{
image.AlternateText = imageUrl;
}
image.RenderControl(writer);
// Wrap up by letting the HyperLink render its end tag
hyperlink.RenderEndTag(writer);
}
else
{
// HyperLink.RenderContents handles a couple of other
// cases if its ImageUrl property hasn't been set. We
// delegate to that behavior here.
base.Render(writer);
}
}
}
Next we need to tell the web site to use the ControlAdapter, this is done by creating a .browser file in the App_Browsers folder:
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.WebControls.HyperLink"
adapterType="MyProject.HyperLinkControlAdapter" />
</controlAdapters>
</browser>
</browsers>
And that's it, no more bug!
Worryingly Microsoft were first told about the bug in November 2006, yet in January 2007 set it's status to 'Closed (won't fix)'. Thankfully there is a nice fix/work-around.
Click here to read the 4guysfromRolla.com article.