Introducing ASP.NET FriendlyUrls - cleaner URLs, easier Routing, and Mobile Views for ASP.NET Web Forms
I've said before how surprised I am that more ASP.NET Web Forms developers don't use Routing to make their URLs prettier. If you don't want "foo.aspx" in your URL, then change it with Routes.MapPageRoute(). However, managing Routing Tables is a little tedious and most WebForms folks aren't used to the concept and don't want to invest the time.
I've also heard a number of ASP.NET Web Forms Developers express a little envy at how easy it is to make a site that has both desktop and mobile views using ASP.NET MVC. They like the idea of seeing an iPhone show up and showing a different view while reusing logic as I've shown in my mobile talks before.
Let's solve both these problems with a new ASP.NET feature just pre-released today in alpha form on NuGet. My peer Damian Edwards and developer Levi Broderick along with QA by Pranav and Anton have come up with a pretty awesome solution based on the original "Smarty Routes" idea from Eilon Lipton and the result is FriendlyUrls.
NOTE: If you've been paying attention to ASP.NET for the last few months you'll recognize this incremental useful but appropriately sized forward motion as being all part of the One ASP.NET master plan.
It's also worth noting that this FriendlyUrls NuGet package includes BOTH an ASP.NET 4.5 and ASP.NET 4 version so .NET 4 folks get love too.
FriendlyUrls Hello World Example
First, the obvious example. Bring up Visual Studio and File | New Project | New ASP.NET Web Forms Application. Now, from the Package Manager Console or from Manage NuGet Packages, install Microsoft.AspNet.FriendlyUrls. You'll need to "Include Prerelease" packages with -pre from the command line or via the dropdown in the UI.
Be sure to read the readme.txt that pops up as you'll need to ensure that the FriendlyUrls routing gets called on application startup! I added this one line to my Application_Start:
RouteConfig.RegisterRoutes(RouteTable.Routes);
Here's the cool part. If I hit one of my existing links, like Contact.aspx, look what happened. See how the GET request for /Contact.aspx turned into a 301 redirect to /Contact?
If you have a Web Form called /Foo.aspx, you automatically get a /Foo route and can call your page like that! Hence, Microsoft.AspNet.FriendlyUrls.
Just by adding the one package and calling
routes.EnableFriendlyUrls();
in RouteConfig (this default came down with the NuGet package) my whole WebForms app loses its .ASPX extensions and gets reasonable defaults.
FriendlyUrls Advanced Sample
Get it? Ok, let's dig into some of the obvious next questions and some more advanced scenarios. How do I get values out of the URL? I'm used to Request.QueryString and Request.Form, but how do I get ahold of these URL segments?
Here's a Foo.aspx that I've visited via /Foo.
If I click "Click Me" the URL points to /Foo/bar/34.
NOTE: Be aware of the magic. It makes sense. If there was a 34.aspx in a folder called Bar in a folder called Foo, we would have used that file. There wasn't. If there was a file called Bar.aspx in a folder called Foo we would have used that. There wasn't. So, we used Foo.aspx and passed in the rest of the URL.
I can get the segments out like this:
<% foreach (var segment in Request.GetFriendlyUrlSegments()) { %>
<li><%: segment %></li>
<% } %>
UPDATE: One thing I forgot to mention was how to get the values out of the FriendlyURL. You can use things like [Form] and [QueryString] to model bind in WebForms. Now you can add [FriendlyUrlSegments] to get data out, like the ID in this example:
public SomeItem SomeItem_GetItem([FriendlyUrlSegments]int? id)
{
SomeItem item = db.SomeItem.Find(id);
return item;
}
They're sitting on the Request option. I did have to import the Microsoft.AspNet.FriendlyUrls namespace to have this extension appear.
<%@ Import Namespace="Microsoft.AspNet.FriendlyUrls" %>
Better yet, I can generate Friendly URLs without string concatenation!
<a href="<%: FriendlyUrl.Href("~/Foo", "bar", 34) %>">Click me</a>
Nice, eh? OK, let's make it mobile.
Mobile Routes with ASP.NET FriendlyUrls
When you bring down the NuGet package you'll also get a Site.Mobile.Master. If I visit them with the Electric Plum Mobile Simulator (iPhone) I see a default mobile page, automatically.
Ah, you see where this is going. I'll copy Foo.aspx to Foo.Mobile.aspx. I'll make a small change. I'll visit /Foo/bar/34 again except now I get the mobile master and the mobile foo, automatically.
What I want to support switching back and forth from Desktop to Mobile? Just add a ViewSwitcher control, also included.
<friendlyUrls:ViewSwitcher runat="server" />
Now I re-render and I get a "switch to mobile" and switch to desktop.
Now I can go back and forth between views and request a desktop site even when on mobile.
So basic mobile is nice but I might want very specific mobile views for iPhone, iPad, Opera Mobile, etc.
Super Advanced Mobile Routes for Specific Devices with ASP.NET FriendlyUrls
By default FriendlyUrls uses a class called WebFormsFriendlyUrlResolver but you can derive from this class and change its behavior however you like. Here's an example of a "DeviceSpecificWebFormsFriendlyUrlResolver" or, better yet, Mobile Friendly Urls for WebForms.
This derived URL resolver does just that, it resolves URLs to physical Web Forms pages. You'd then pass it into the overload of EnableFriendlyUrls(...);
IMPORTANT NOTE: This code is just a very early sample, there will be a more complete one released later.
public class DeviceSpecificWebFormsFriendlyUrlResolver : WebFormsFriendlyUrlResolver
{
private readonly IDictionary<string, string> _deviceUserAgentMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "Opera Mobi", "OperaMobile" },
{ "iPhone", "iPhone" },
{ "iPad", "iPad" }
};
protected override IList<string> GetExtensions(HttpContextBase httpContext)
{
var extensions = base.GetExtensions(httpContext).ToList();
if (extensions.Contains(MobileAspxExtension, StringComparer.OrdinalIgnoreCase))
{
// Base has determined we should look for a mobile page, let's add device specific
// extension to the beginning.
var deviceSpecificSufffix = GetDeviceSpecificSuffix(httpContext);
if (!String.IsNullOrEmpty(deviceSpecificSufffix))
{
extensions.Insert(0, "." + deviceSpecificSufffix + AspxExtension);
}
}
return extensions;
}
protected override bool IsMobileExtension(HttpContextBase httpContext, string extension)
{
return base.IsMobileExtension(httpContext, extension) ||
_deviceUserAgentMap.Values.Any(v => extension.Contains(v, StringComparison.OrdinalIgnoreCase));
}
protected override bool TrySetMobileMasterPage(HttpContextBase httpContext, Page page, string mobileSuffix)
{
var deviceSpecificSufffix = GetDeviceSpecificSuffix(httpContext);
if (!String.IsNullOrEmpty(deviceSpecificSufffix) && base.TrySetMobileMasterPage(httpContext, page, deviceSpecificSufffix))
{
// We were able to set a device specific master page, so just return
return true;
}
// Just use the base logic
return base.TrySetMobileMasterPage(httpContext, page, mobileSuffix);
}
private string GetDeviceSpecificSuffix(HttpContextBase httpContext)
{
foreach (var item in _deviceUserAgentMap)
{
if (httpContext.Request.UserAgent.Contains(item.Key, StringComparison.OrdinalIgnoreCase))
{
return item.Value;
}
}
return String.Empty;
}
}
Now we've created a map of device specific suffixes, so we can have not Foo.Mobile.aspx, but rather Foo.iPhone.aspx and Foo.OperaMobile.aspx, etc.
Here's a little demo that loads a bunch of names into a list. Here's /async, the desktop view.
Now we'll add jQuery mobile to the mobile master page, and use it on the mobile version of the same page. We're still calling the same data source and reusing all that code.
I'm pretty jazzed about what this means for ASP.NET and Web Forms developers. We're going to continue to push forward and improve ASP.NET even now, after Visual Studio 2012 has been released. Sometimes we'll add small features via NuGet packages, sometimes editor improvements as free VSIX Extensions like the Web Essentials playground for 2012 and larger changes via released updates to all of ASP.NET. I hope you like the direction we're heading.
Go play with Microsoft.AspNet.FriendlyUrls now and thank Damian and friends on Twitter!
This week's sponsor: Be part of GENERATION APP. Your Idea. Your App. 30 Days. Begin your 30-day journey to create a Windows Store style app and talk 1-on-1 with a Windows 8 app development pro. Get started today.
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Looking forward to it. Thank you
Can the QueryString Value provider still be used herewith or must one make use of the Request object as indicated in the sample?
It may be helpful to see (or try) the implications of the magic behind physical files vs parameters, but I bet most applications have these sorts of magic hanging non-measured (and not having notable performance cost) anyway.
However, this was generally fine with the routes support that already exists now. sure it's easier and it's hence welcome and everything, but the one really wanted feature in ASP.NET web forms I'd guess is: Multiple forms! I know it's probably very difficult to implement with the current webforms architecture though, and wouldn't be as helpful for existing projects, but for new projects, that would be the only reason I'd "happily" create a new webforms project.
Thanks for sharing. Special thanks to Damian and Pranav :)
http://msdn.microsoft.com/en-us/library/system.web.ui.control.viewstatemode.aspx
Multiple DisplayModes - Caching error, will show wrong View
That bug is killing me, because it essentially breaks the alternate views. If they have different or additional @sections, and the cache breaks, you get 500's all over the place.
FriendlyUrl.HrefWithQueryString("~/Foo", "bar", 34) %>
which ends up being
http://www.something.com/foo?bar=34
does this work natively in IIS5.1 or would it be like routing for MVC and we would still need to add a custom mapping to the aspnet_isapi?
Nice work on the urls, keep it up - looking forward to when it will be easy to configure at runtime.
Anyway, it looks like FriendlyUrls may make up for the lost keystrokes - thanks!
when we started use following syntax in .aspx page, what made it work?
<% foreach (var segment in Request.GetFriendlyUrlSegments()) { %> <li><%: segment %></li><% } %>
Thanks!
Xiaohong Liu
Added FriendlyUrls to my .NET 4 site, it works!
Deploying it as-is yields this error, "Request for the permission of type 'System.Security.Permissions.SecurityPermission' failed".
Changing the trust level is not allowed at the current host.
thanks,
-brit
Is there any easy fix for this?
I try hitting the URL with several Android phone browsers and it takes me to the Desktop templates by default.
Anyone have any insite on how to fix this issue for Android phones?
I assumed that :
1)< a href="~/About.aspx?id1=123&id2=987" runat="server">Tester < /a> would automaticly be transformed into a friendly version
2) Request.QueryString["id1"] would automaticly work when having a friendly url like "/About/id1/123"
But now we have to rewrite all our links manually AND we have to rewrite all our "getters" with the use of "GetFriendlyUrlSegments()"
Is this true or do I miss something crucial and that it already works this way? :)
using System.Web.Routing;
into global.asax as well not only
RouteConfig.RegisterRoutes(RouteTable.Routes);
great article!
I'd like to use this package but I get the following error:
"The name 'RouteConfig' does not exist in the current context".
I just added:
using System.Web.Routing;
...
protected void Application_Start(object sender, EventArgs e)
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
Could you help me please?
I'm using VS 2012 (VB.NET). I have 2 questions:
1. When I call Routes.enablefriendlyurls as indicated in this article, I get the following error: Routes.enablefriendlyurls is not a member of System.Web.Routing.RouteCollection. Why?
2. Take the following example:
How can I rewrite the url Index.aspx?articleid=123456&languageid=EN as
"Index/123456/EN"
Does someone have a working example in VB or C#?
You can mail it to mobileboy36@gmail.com
Thank you group!
Best regards.
Clean asp.net Webforms 4.5 project.
"The name 'RouteConfig' does not exist in the current context".
using System.Web.Routing;
...
protected void Application_Start(object sender, EventArgs e)
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
It seems like the latest build split out some features and dependencies into 2 packages.....just wondering what was the reason why the first package didn't also automatically pull in the 2nd package?
Yes you are right, include both packages and it will work. This will be fixed when it releases.
1# If i call my URL passing parameters, like:
www.mywebsite.com/page/parameter
The "page.aspx" is called two times... (not post back) it´s two different requests by differents threads.
2# if my page have images with status 404 (not found images) and if i´m calling this page with parameters, i have N different requests o my page, where N = Number of images with 404 status.
could someone help-me?
Microsoft.Web.Infrastructure seems to be required but is not loaded as a dependency.
Is there a more complete example for using the ViewSwitcher, I am using the standard Site.Master from Visual Studio 2012 and the Site.Mobile.Master does not look anything like it, it looks like something from Visual Studio 2008 or 2010.
I am having problems trying to perform an .ajax() call with friendly URLs enabled
when
: the url = 'MyPage.aspx/MyMethod', i received a 401 Authentication failed error.
: the url = 'MyPage/MyMethod', i received the entire page page in the response
after I've commented out the "routes.EnableFriendlyUrls();"
the call is successful with the expected Response.
Is there any workaround for this?
$.ajax({
type: "POST",
url: 'MyPage.aspx/MyMethod',
data: {},
contentType: "application/json; charset=utf-8",
dataType: "text",
async: true,
cache: false,
success: function (result) {
alert('success');
},
error: function (x, e) {
alert(x.responseText);
}
Thanks
Adlie
I'm trying this new FriendlyUrls feature but for some reason I don't get that 301 redirect when I access the page with the ".aspx" extension. If I remove it manually everything works as expected.
Any idea why?
I really only want the simplified way of having friendly urls.
We're designing a website using media quieries so the mobile view will be delivered based on that.
Yes, we can split the responsive media queries into seperate css files to work with the mobile master...but it's not exactly clean since we'll now have 2 master pages to update (ie if menu needs updating)
Nothing in the resolver class is called and nothing .mobile ever gets called. I am using Visual Studio 2012 and VB. I did not notice any changes in web.config, yet the file get refreshed in the IDE, could that be the issue?
I have added the two lines below and they have had no effect.
Dim settings As New FriendlyUrlSettings
settings.AutoRedirectMode = RedirectMode.Permanent
routes.EnableFriendlyUrls(settings)
The class RouteConfig is not found.
VS 2010 VB.net Project?
1. Install the 2 following NuGet Packages
- Install Microsoft ASP.NET Friendly URLs - Core
- Install Microsoft ASP.NET Friendly URLs.
2. Read the "readme.txt" that gives you the next instructions.
3. Update your Global.asax.
<%@ import Namespace="System.Web.Routing" %>
4. Add the following call in your Application Start of Global.asax:
RouteConfig.RegisterRoutes(RouteTable.Routes)
Note: If your getting the error "RouteConfig" is not found you didn't install both friendly url packages above.
Tech Note: The Friendly Url Core Package will install a New Start Folder that contains classes and methods to support friendly urls. In VS 2010 look for the App_Start folder under the App_Core folder.
That's it, I hope this helps.
These instructions are for the ASP.NET Friendly URLs v1.0.0. Not the Pre-Release.
Mike - Giving Back after taking so much. :)
Does anyone know how to use PageMethods with this AspNetFriendlyUrl's enabled?
Thanks
Adlie
Or any web application projects?
Or only web application projects?
I started my project as Empty web application and added my webforms myself.Can these be the problem.
I manually removed .aspx suffix from all refernces, as Scott explained i thought that referncing the package will automatically remove the suffix.
Imports System
Imports System.Collections.Generic
Imports System.Web
Imports System.Web.Routing
Imports Microsoft.AspNet.FriendlyUrls
Namespace ASP
Public Module RouteConfig
Public Sub RegisterRoutes(routes As RouteCollection)
routes.EnableFriendlyUrls()
End Sub
End Module
End Namespace
Guys, here is the big secret: the customers do not care how your site's URLs look. The only URLs a user types into the browser are filenameless home page URLs that use default pages. The rest are only used in links. Users care about as much about how URLs look as they care about the style of a web site's source code.
The big value of URL rewriting is for mobile views to more easily support mobile devices. If you are using URL rewriting just to make your URLs prettier, you are wasting your time.
This is a nice ASP.NET UrlRouting tool I have ever tried nowadays. I devepoed a asp.net web form app using this tool. Its great working my local IIS. If I deploy to my web hosting, its giving an error about SecurityPermission.
Please help me!
Thanks a lot.
Exception: System.Security.SecurityException
Message: Request for the permission of type 'System.Security.Permissions.SecurityPermission, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
StackTrace: at System.Web.Hosting.HostingEnvironment.RegisterObject(IRegisteredObject obj)
at Microsoft.AspNet.FriendlyUrls.Abstractions.HostingEnvironmentWrapper.RegisterObject(IRegisteredObject obj)
at Microsoft.AspNet.FriendlyUrls.Caching.StaticFileCacheRunner.GetAllFilenamesInApplication()
at Microsoft.AspNet.FriendlyUrls.Caching.StaticFileCache.<>c__DisplayClass2.<.ctor>b__0(Object _)
at System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
at System.Threading.ExecutionContext.runTryCode(Object userData)
at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
I am having the same issue with GoDaddy. The odd thing is it was working when I had basic hosting, when I upgraded to Deluxe Hosting it stopped working. Deluxe hosting also came with ASP.NET 4.5 instead of 4.0 so that might have something to do with it. GoDaddy's solution was to disable global.asax on my account (not useful).
I know GoDaddy does not support "FullTrust" on shared accounts but that would not explain why it works on Basic Hosting.
Anyone know what is going on?
It is the same as described here:
http://forums.asp.net/t/873722.aspx/1?Locating+Javascript+file+using+Friendly+URLs
it happens that if you use FriendlyUrl.Href("~/Details", "param") and have the URL "/Details/param?key=value" that all the files used in script tags like src="js/details.js" the request is sent to the page with the request URL "Details/js/details.js" instead of "js/details.js"
If you don't use any parameter after the page name it works fine. Is there any solution without rewriting all references in the project?
Any relative paths in that page will now be treated as relative to ~/Details, as you’d expect.
To fix it, as always, scripts and other resources should not be referenced statically, but rather resolved from app relative root, e.g. ResolveUrl("~/js/details.js")
Maybe it is better to rewrite all inline included references to ResolveUrl("~") for static ressources anyway.
And read the one-line configuration here: https://github.com/AtaS/lowercase-dashed-route
routes.MapPageRoute("Search", "search-directory/{searchterm}", "~/Directory.aspx");
but Request.GetFriendlyUrlSegments() returns nothing in Directory.aspx. How can I get the parameter passed?
This is awesome plugin for URL friendly rewrite.
I Just found on Google and i was trying to on my project but it is not working for me.
it giving me errors are below :
1. "Could not load type 'ASP.Site_Mobile' "
2. "Could not load type 'ASP.ViewSwicther' "
Please help me.
Thanks
<% foreach (var segment in Request.GetFriendlyUrlSegments()) { %>
<% } %>
// C#: Several methods are applicable to: ... same methods though.
this worked:
<% foreach (var segment in HttpRequestExtensions.GetFriendlyUrlSegments(this.Request)) { %>
<% } %>
Will this work in a situation where one uses web forms within MVC?
I'm asking this because my MVC projects have web forms that host reports using the Report Viewer Control.
e.g. I want only iPhone to be classed as a mobile device and all others to be served the desktop version by default.
Thanks again!
DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("iPhone") {
ContextCondition = ctx => ctx.GetOverriddenUserAgent().Contains("iPhone")
});
But there's a thing i dont know how it works.
How can i get querystring mapped values?
I know its possible via foreach ...segments
But, is there any way for getting a value like in Request.QueryString["id"] ?
Thanks in advance.
routes.MapPageRoute("NewsRouteArticle", "news/article/{url}", "~/news.aspx")
The URL is matched against a news article in the database.
What happens if a news article no longer exists? How can a handle this and redirect the user to the Custom Error page? I'm aware of the issue that exists with ASP.NET where 404 Errors actually give a status code of 302 and want to try and avoid this issue (see http://imar.spaanjaars.com/497/proper-handling-of-404-errors-using-redirectmode).
I just saw your questions last night, I was also facing same problem with FriendlyURLs. I know its too late but I have found a solution by my self.
Use direct call to a C# function from ajax, here is a post, it will help you how to call a C# function from ajax.
<a href"http://encosia.com/using-jquery-to-directly-call-aspnet-ajax-page-methods/">http://encosia.com/using-jquery-to-directly-call-aspnet-ajax-page-methods/</a>
routes.Add(new Route("{resource}.axd/{*pathInfo}", new StopRoutingHandler()));
Jeremy - regarding error pages... here is my routeconfig.cs entry
routes.MapPageRoute("errors", "error/{friendlyurl}/{*pathInfo}", "~/error/default.aspx");
and the web.config contains...
<customErrors mode="On" defaultRedirect="/error/technical-difficulties" redirectMode="ResponseRewrite">
.....
<error statusCode="404" redirect="/error/file-not-found"/>
</customErrors>
while the <system.webServer> entry is
<httpErrors>
....
<remove statusCode="404" subStatusCode="-1"/>
<error statusCode="404" prefixLanguageFilePath="" path="/error/file-not-found" responseMode="ExecuteURL"/>
....
</httpErrors>
Although, I would favor a message stating the news article does not exists when no records are returned from a database, on the news article page and use the 404 response for a more catch all type scenario.... It all depends on how your content is driven.
Hope this helps.
if (dT.Rows.Count == 0)
{
//send email to dl web development about link expiration.
litCmsContent.Text = helpers.notFoundText();
Response.StatusCode = 404;
Response.Status = "404 Not Found";
}
My sitemap contains urls with the .aspx extension. I don't want to change this because then the option securityTrimmingEnabled="true" of my sitemap provider won't work as expected.
Could you point me in the right direction in order to fix this?
Comments are closed.