Rick Strahl's FoxPro and Web Connection Web Log
White Papers | Products | Message Board | News |

Extensionless Urls with Web Connection and the IIS UrlRewrite Module


Thursday, August 2, 2012, 12:02:12 AM

In the last few months I've gotten a lot of requests from people who want to use extensionless URLs in their Web Connection applications. Extensionless URLs are URLs that - as the name implies - don't have an extension, but rather terminate in what looks like a directory. For example, you might expose customer data with formats like these:

http://localhost/wconnect/customers
http://localhost/wconnect/customers/32131

As you can see the URLs here don't appear to be ending in a 'file name' like index.htm or MyPage.wcsx etc. Rather they end in what looks more like a directory path. While it's mostly semantics, there's a lot of thought on the Web surrounding 'clean urls' like the above, that better describe Web resources. Extensionless Urls tend to use the URL path rather to describe the 'routing' and parameters of a request rather than more traditional approaches using script file links coupled with query strings; although querystrings are supported and often necessary still even on extensionless Urls. When using extensionless URLs we often think about 'nouns' or 'resources' (ie. Customers) rather than actions (GetCustomer.wc or GetCustomerList.wc).

Long story short, extensionless URLs are not something that have been natively supported in Web Connection in the past. There are a number of ways that this can be accomplished - here are a couple:

  • Wildcard ScriptMaps
    Wildcard scriptmaps essentially allow you to route EVERY request the Web server receives to a specific extension like wc.dll or the .NET managed handler. You then need to override the default Web Connection processing to handle the incoming URLS.

  • The UrlRewrite Module for IIS7+
    IIS 7 provides for a downloadable UrlRewrite module that can be plugged into IIS and that provides a rules based engine that can either rewrite or redirect URLs. Url rewriting basically takes an incoming URL that matches and rewrites it as another - in the process changing all the path based IIS request data (like LogicalPath,PhysicalPath,ScriptName etc.), while keeping the other request data like QueryString() and Form() intact. Unlike a Redirect operation, a rewrite only changes the URL your application sees to the new URL. This process is better suited for Web Connection because it allows some control over what URLs specifically are routed to Web Connection.

For Web Connection applications Wildcard ScriptMaps are generally NOT a good idea because Wildcard scriptmaps map EVERYTHING to the Web Connection server including images, css, scripts, static HTML etc. and Web Connection (and any dynamic content engine really) has quite a bit of overhead compared to the static file services and the caching that IIS natively provides. This is really not a good match for Web Connection.

UrlRewrite on the other hand allows you to specifically create a rule that filters URLs and sends matches (or non-matches in our case) to another URL by 'rewriting' the URL - that is the paths are updated to reflect the new URL while the browser's address bar retains the original URL. This is much more efficient for Web Connection because it'll only get hit by requests that match the search criteria. The UrlRewrite approach is what I'll discuss in this post.

The IIS UrlRewrite Module

You can download the URL Rewrite Module for IIS7 and later by using the Microsoft Web Platform Installer. Find the UrlRewrite Module in the Products | Server and select Url Rewrite 2.0:

 WebPlatformInstaller

and install from there. The installer does all the work of pulling the module and any dependencies.

Once the module is installed you can enable it in your  Web Connection virtual or root directory by creating a rewrite rule. As most IIS 7 settings, rewrite rules are stored in web.config. Here's a rule that rewrites extensionless URLs:

    <system.webServer>
         <rewrite>
            <rules>
                <rule name="ExtensionLessUrls" patternSyntax="Wildcard" stopProcessing="true">
                    <match url="*.*" negate="true" />
                    <conditions>
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                    </conditions>
                    <action type="Rewrite" url="UrlRewriteHandler.wwd" appendQueryString="true" />
                </rule>
            </rules>
        </rewrite>        
    </system.webServer>

The above is pretty much cut and paste - you only need to change the Rewrite action url to ensure it points to a valid URL for your virtual directory.

Alternately you can also enter rewrite rules from the IIS Management Console:

RewriteIISManagementConsole

What this rule says basically is this:

  • Don't match files that have a . in the name (ie. no extension)
  • Match files that are not a physical directory
  • Match files that are not a physical file
  • If true Rewrite the URL to UrlRewriteHandler.wwd and forward the query string data from the original url

In this case I specified that I want any extensionless URL to be re-written to UrlRewriteHandler.wwd which points at my wwDemo process class example which has a .wwd extension.

Note that this uses WildCard pattern matching - you can also use RegEx expressions matching instead. Here I look for a . in the URL, which will work as long as none of your paths in your site contain '.'.

Creating a FoxPro Handler for Extensionless Urls - UrlRewriteHandler

Prior to Web Connection 5.64 UrlRewriteHandler does not exist but as of 5.64 there are a couple of special handler methods in the wwProcess that can act as an endpoint for extensionless URLs. Basically when you create the Rewrite route you can point it at UrlRewriteHandler.wwd (use your own Process Class scriptmap here instead of wwd) and off you go.

If you're using a version prior to 5.64 you can implement UrlRewriteHandler and OnUrlRewrite like this either on your wwProcess subclass or on wwProcess itself as is the case for 5.64 and later:

************************************************************************
* wwProcess ::  UrlRewriteHandler
****************************************
***  Function: Handler called when a UrlRewrite occurs via 
***            Specify UrlRewriteHandler.wwd (use your extension)
***            as the endpoint for extensionless URLS and then 
***            implement OnUrlRewrite() in your process class.
************************************************************************
FUNCTION UrlRewriteHandler()
LOCAL lcOrigUrl, lnItems, loRewrite, lcQuery, lnX
LOCAL ARRAY laTokens[1]
 
*** Rewrite URL injects the original URL as a Server Variable
lcOrigUrl = Request.ServerVariables("HTTP_X_ORIGINAL_URL")
IF EMPTY(lcOrigUrl)
   lcOrigUrl = Request.GetExtraHeader("HTTP_X_ORIGINAL_URL")
   IF EMPTY(lcOrigUrl)
       *** or pull it off the querystring
       lcOrigUrl = Request.QueryString("url")
   ENDIF
ENDIF
 
*** Create result object
loRewrite = CREATEOBJECT("Empty")
ADDPROPERTY(loRewrite,"cOriginalUrl",lcOrigUrl)
 
*** Split path and query string
lnItems = ALINES(laTokens,lcOrigUrl,1 + 4,"?")
IF lnItems > 0
   ADDPROPERTY(loRewrite,"cOriginalPath",laTokens[1])   
ELSE
   ADDPROPERTY(loRewrite,"cOriginalPath",lcOrigUrl)   
ENDIF
 
*** Split path into Collection
loSegments = CREATEOBJECT("wwCollection")
lcVirtual = STRTRAN(LOWER(Request.GetVirtualPath()),"/","")
 
lnItems = ALINES(laTokens,loRewrite.cOriginalPath,1 + 4,"/")
FOR lnX = 1 TO lnItems
    lcToken = laTokens[lnX]
    IF !(LOWER(lcToken) == lcVirtual)
        loSegments.Add(lcToken)
    ENDIF
ENDFOR
 
ADDPROPERTY(loRewrite,"oPathSegments",loSegments)
 
THIS.OnUrlRewrite(loRewrite)   
 
ENDFUNC
*  wwProcess ::  UrlRewriteHandler

The RewriteUrl method acts as the endpoint for a URL like UrlRewriteHandler.wwd which should be plugged into the rewrite rule. When an extensionless URL is fired it then fires this method which in turn picks up the original URL via the HTTP_X_ORIGINAL_URL header that IIS injects into the Request data. This header contains the original URL which then method then forwards along with a couple of parsed variables, to the OnUrlRewrite method. The OnUrlRewrite method is a convenience method you can easily override when a hit occurs that receives this parsed object.

The loRewrite object parameter has the following properties:

Property Function
cOriginalUrl The original extensionless URL that triggered the rewrite. Full server relative path that includes the query string.
/wconnect/customers/3211?parm=val
cOriginalPath The original extensionless URL that triggered the re-write, without query string.
/wconnect/customers/3211
oPathSegments Each of the path's segments relative to the virtual directory or root in a wwCollection instance.
Two segments: customers and 3211
lcId = loRewrite.oPathSegments.Item(2)

which makes it fairly easy to do something useful with the data passed.

A simple example of an Extensionless Url Handler

Let's look at a somewhat simplistic example implementation of an Extensionless URL handler that basically routes extensionless requests to requests with a known extension.

It would allow you to effectively create a handler for your application that maps a URL like:

http://localhost/wconnect/TestPage

and route them to the TestPage method in your process class.

The implementation of such a handler can be very basic:

************************************************************************
* wwDemo ::  OnUrlRewrite
****************************************
FUNCTION OnUrlRewrite(loRewrite)

this.oRewrite = loRewrite

*** Assume second segment is our method name
IF loRewrite.oPathSegments.Count > 0
   lcMethod = loRewrite.oPathSegments.Item(1)   
   RETURN EVALUATE("THIS." + lcMethod  + "()")   
ENDIF

this.ErrorMsg("Invalid Route",;
   "Route values must at least include 1 segments relative to the virtual or root application")

ENDFUNC

Basically this code does little more than checking for the first segment (TestPage) and assuming that this segment  is the name of the method you want to call in the process class. It takes the method and then simply Evaluates the method. Since the rewrite keeps the QueryString() and Form() data intact the behavior of the method is almost the same as if you called it directly with:

http://localhost/wconnect/TestPage.wwd

There is a difference however, between a rewrite to this URL and directly accessing this URL: All the paths inside of the TestPage method on the rewrite point to UrlRewriteHandler.wwd - not the original Url or testpage.wwd because the rewrite rule points at UrlRewriteHandler.wwd. The original extensionless URL is only available as a parameter to the OnRewriteUrl(loRewrite) method. So things like Request.GetPhysicalPath(), GetLogicalPath(), GetExecutablePath(), GetCurrentUrl() etc. will point at UrlRewriteHandler.wwd, so be aware that if you simply want to map existing scriptmapped extensions to non-scriptmapped extensions there might be pathing issues.

Using multiple Path Segments

Let's expand on that last example with something a little more practical and consider creating a handler for the following two URLs which return customer information:

http://localhost/wconnect/customers

http://localhost/wconnect/customers/West+Wind+Technologies

Here I have two URLs that return a list of customers and a specific customer respectively. In order to do this, I need to make a slight change to the OnRewrite method to capture the loRewrite object and make it available to my handlers and store it in an oRewrite object:

oRewrite = NULL

************************************************************************
* wwDemo ::  OnUrlRewrite
****************************************
FUNCTION OnUrlRewrite(loRewrite)

this.oRewrite = loRewrite

*** Assume second segment is our method name
IF loRewrite.oPathSegments.Count > 0
   lcMethod = loRewrite.oPathSegments.Item(1)   
   RETURN EVALUATE("THIS." + lcMethod  + "()")   
ENDIF

this.ErrorMsg("Invalid Route",;
   "Route values must at least include 1 segments relative to the virtual or root application")

ENDFUNC

************************************************************************
*  wwDemo :: Customer
****************************************
***  Function:
***    Assume:
***      Pass:
***    Return:
************************************************************************
FUNCTION Customers()

lcId = ""
IF this.oRewrite.oPathSegments.Count > 1
    *** Retrieve second parameter segment and query
    lcId = this.oRewrite.oPathSegments.Item(2)
ENDIF

*** No id passed - display a list
IF EMPTY(lcId)
    SELECT * FROM tt_cust ;
       INTO CURSOR TQuery ;
       ORDER BY Company   
    lcHtml = HtmlDataGrid("TQuery")
ELSE
    SELECT * FROM tt_cust ;
       WHERE company = lcId ;
       INTO CURSOR TQuery
    lcHtml = HtmlRecord("TQuery")
ENDIF

THIS.Standardpage("Show Customer",lcHtml)
ENDFUNC
*   Customers

The Customers method is now implemented in such a way that it can look at the second path segment to determine the ID of the customer to look up. If no second segment exists - assume that a customer list gets displayed. If there is an ID segment - load the individual customer and display it.

Both of those URLs can now be handled fairly easily as you can see.

Note that this is just one example of how you can handle your routing. I suspect in other real-world scenarios the logic to determine which method needs to be fired in OnUrlRewrite() might be more complex especially if URL paths nest several levels deep. You can apply more complex rules in the OnRewriteUrl method to fit your needs.

Feedback Wanted

To be honest I haven't had much need to build extensionless URLs with FoxPro, so I don't have a real good idea what people are planning to accomplish with URLs. I'm pretty familiar with ASP.NET MVC style routing where you can add route parameters which is a possible future addition for Web Connection when using the Web Connection .NET Handler.

For the moment I'm curious to hear feedback on what you need to accomplish and/or whether the relatively simple implementation outlined here would address your needs or how you think it might be improved. Any feedback would be greatly welcome.

Posted in: FoxPro    Web Connection

 



© Rick Strahl, West Wind Technologies, 2003 - 2018