Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats – natively work with DOCX, XLSX, PPT, PDF, images and more

Routes, Extensionless Paths and UrlEncoding in ASP.NET


UrlEncoding in Web applications can be a pain and in .NET, with its various utilities that all behave slightly differently for various edge cases, doesn't make it any easier. I wrote about the pain of UrlEncoding in .NET before. The resolution of that previous post was that Uri.EscapeDataString() is as close as it gets to a best solution out of the box.

But even with that knowledge I ran into trouble again with this topic, this time with URL paths created as part of an update to an old WebForms application and adding routing in order to provide cleaner URLs for accessing the product pages and categories. Here I'm not actually encoding query string parameters or post values, but instead encoding path segments on URL routes.

Essentially, I created  extensionless URLs for a few select URLs of the application. When encoding extensionless URLs extra care has to be given to properly encoding path strings, as paths are more sensitive to special rules that determine how the paths are parsed by the Web server and ASP.NET.

Routing 101 in Web Forms

The process of adding routing features to an old Web Forms application is pretty straight forward and using a few MapPageRoute() calls make short work of this process.

In this case I'm routing urls in my Web Store by mapping out products and categories like this (fired off global.asax's Application_Init()):

// Specific Route mapping
routes.MapPageRoute("ProductPage", "product/{sku}", "~/item.aspx");
routes.MapPageRoute("ProductPageWithQty", "product/{sku}/{qty}", "~/item.aspx");
            
// List Views Routings
routes.MapPageRoute("ProductCategory", "products/{category}", "~/itemlist_abstract.aspx");
routes.MapPageRoute("ProductWithoutCategory", "products", "~/itemlist_abstract.aspx");

This turns urls like:

item.aspx?sku=ProductID

into

product/ProductId

and

itemlist.aspx?Category=Books

into

products/Books

Note that unlike MVC, there is no direct support for optional parameters in MapPageRoute(), so each path configuration requires its own explicit route config.

So far so good. This is nice and easy to accomplish even in a WebForms application. This is an old app so I only updated a few URLs that are the most commonly externally accessed and crawled links, but it would be easy enough to do most of the application links using a similar approach.

Capturing the Route Data in WebForm

Capturing the RouteData in the routed pages is also very easy to do. Previously the code was capturing the query string, now the code captures both query string and the RouteData collection for checking the url parameters. Here's the item SKU and QTY mapping logic:

void GetSkuAndQty()
{
    Sku = Request.QueryString["Sku"];
    if (string.IsNullOrEmpty(Sku))
    {
        Sku = RouteData.Values["sku"] as string;
    }
    string Qty = Request.QueryString["qty"];
    if (string.IsNullOrEmpty(Qty))
    {
        Qty = RouteData.Values["qty"] as string;
    }

    // redirect permanently to new url
    if (Request.Url.AbsoluteUri.Contains(".aspx"))
    {                
        if (!string.IsNullOrEmpty(Sku))
        {
            string newUrl = "~/product/" +Sku + "/" + Qty;
            Response.RedirectPermanent(ResolveUrl(newUrl));
        }
    }
}

From here the code is identical to the original code, using the Sku and Qty to load the Item business object and displaying the inventory item purchase UI.

Creating Route Links Manually

In many places of the application the URLs to link to the product and category pages are generated, meaning that the links are generated as well. It seems easy enough, using code like this (for the category list):

ItemListForm = ResolveUrl("~/products");

foreach
(LineItem item in LineItems) { sb.AppendFormat(@"<div class='menurow'><a class='menulink' href='{0}/{1}'>{2}</a></div>", ItemListForm,
Uri.EscapeDataString(item.Category),
HttpUtility.HtmlEncode(item.Category)); }

Note that that I URL encode the category for the URL and HtmlEncode the category for the display text.

This produces Urls like:

products/Books and products/Development%20Tools. We're golden, right?

Posted in ASP.NET  C#  

The Voices of Reason


 

Matt
November 16, 2013

# re: Routes, Extensionless Paths and UrlEncoding in ASP.NET

Rick,

Regarding the ".NET" path problem, does the web.config setting below offer a solution?

http://msdn.microsoft.com/en-us/library/system.web.configuration.httpruntimesection.relaxedurltofilesystemmapping(v=vs.110).aspx

Matt

Graham Mendick
November 16, 2013

# re: Routes, Extensionless Paths and UrlEncoding in ASP.NET

Great article, Url's don't get the respect they deserve. I've written a Navigation framework for Web Forms that solves these problems, https://navigation.codeplex.com/.
For example,
StateController.GetNavigationLink("Products", new NavigationData { { "category", "c#" } });

makes the link products/c%23/ And
StateController.GetNavigationLink("Products", new NavigationData { { "category", ".NET" } });

makes the link products/.NET/ (as long as you set the RouteCollection.AppendTrailingSlash to true)

Rick Strahl
November 17, 2013

# re: Routes, Extensionless Paths and UrlEncoding in ASP.NET

@Graham - nice! Thought about doing something like that a while ago but never got around to it... This looks great and should have been in the box as part of the standard ASP.NET Routing features. There's no reason the MVC features should have to be MVC specific.

Jesper
November 30, 2013

# re: Routes, Extensionless Paths and UrlEncoding in ASP.NET

A widely-used practice to make this sort of thing work without having to worry about escaping is to create a "slug" - a deformed, normalized version of the name using only a basic character set like a-z, numbers and dashes. Make a slug for every category and use the slug in the route instead of the escaped full fidelity name and having to sort out encoding will not become a problem. /products/csharp is an easier path to type, remember and guess than /products/c%23.

Since this post already has a slug itself ("Routes-Extensionless-Paths-and-UrlEncoding-in-ASPNET"), I suspect you already know about this. It's a shame it wasn't mentioned in the post though since, even though it requires changes to the data, it's a simple pattern that works with the URL instead of against it and allows you to solve this problem once and move on.

Rick Strahl
September 07, 2014

# re: Routes, Extensionless Paths and UrlEncoding in ASP.NET

@Jesper - yes, slugs are a good idea for relatively long and unique content. But if you have things like categories or other groups of short named things it's very easy to have slugs with the same name as you're either omitting or converting characters to custom characters, so that can become problematic. Slugs work great for things like page titles, but for meaningful values that need to essentially do a database lookup on and where it doesn't make sense to store the slug in the db it's not such a good idea.
 

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2017