MonoRail - URL Rewriting and Persisting QueryString Parameters

In my last post I talked about how each of our customers identify themselves to our app via a CustomerId query string parameter.  Rather than give links to our customers with ?CustomerId=12345 sprinkled about, I thought it would be nice to take advantage of the built in URL rewriting capabilities of MonoRail.  My target URLs have the following form, https://my.product.com/customerId/controller/action.rails and this will get rewritten to https://my.product.com/controller/action.rails?CustomerId=customerId.  Turns out this is a very simple configuration change.  URL rewriting is handled by the routing service in MonoRail.  You can configure custom 'routes' by adding a subsection to the monorail config section in your web.config file.  You also need to add the routing HTTP module to your list of loaded modules.  Here is what I added to get my desired result (adapted from the Exesto routing configuration):

<routing>
  <rule>
    <pattern>/(?'customerId'\d+)/(?'controller'\w+)/(?'action'\w+)\.rails(\?(?'queryString'.*))?</pattern>
    <replace><![CDATA[ /${controller}/${action}.rails?CustomerId=${customerId}&${queryString} ]]></replace>
  </rule>
</routing>
<httpModules>
  <!-- The order these are listed is important -->
  <add name="routing" type="Castle.MonoRail.Framework.RoutingModule, Castle.MonoRail.Framework" />
  <add name="monorail" type="Castle.MonoRail.Framework.EngineContextModule, Castle.MonoRail.Framework" />
</httpModules>

A few things to note.  We aren't currently using Areas and this routing rule would conflict with the default routing.  Make sure you load the routing module before the monorail module or your custom routes may not work.  Happy day!  We get prettier URLs and we still uniquely identify each customer's site which means they see their custom UI and execute their custom business logic.  There is one small issue though.  Whenever we LinkTo, Redirect, or BeginFormTag, we will need to take the CustomerId and make sure it is injected in the right place in our URLs.  Ugh!  big pain, no thank you.  MonoRail to the rescue! 

The beauty of MonoRail is that it is composed of a number of independent services that can be replaced at will with custom implementations.  The difficult task is determining where is the best place to inject the custom functionality you desire.  In my previous post we implemented runtime views and layouts using a Filter.  Is this the only way we could have achieved it?  No, but it was probably the most logical and the way MonoRail was intended to be extended.  There are a number of ways I can extend MonoRail to ensure that the CustomerId querystring parameter is persisted from request to request.  I chose to make my extension to MonoRail's UrlBuilder.  The UrlBuilder is in charge of...building URLs :)  It allows you to pass in an area, controller, and action, and it will return a well formed URL to said resource.  This seemed like the most logical place to add something to a URL.  I derive from DefaultUrlBuilder and override InternalBuildUrl which is the method used to generate final URLs (I've submitted a patch to make this method virtual, should be in the trunk in the next few days).  I simply check to see if CustomerId is already on the QueryString.  If so, it is inserted into the proper place to generate a URL that will be recognized by the routing rule we implemented above.

namespace My.Product.Mvc
{
  using Castle.MonoRail.Framework;
  using Castle.MonoRail.Framework.Services;

  public class AppendCustomerIdUrlBuilder : DefaultUrlBuilder
  {
    protected override string InternalBuildUrl(string area, string controller, string action, string protocol, string port,
                                               string domain, string subdomain, string appVirtualDir, string extension,
                                               bool absolutePath, bool applySubdomain, string suffix)
    {
      if( !absolutePath )
      {
        string customerId = MonoRailHttpHandler.CurrentContext.Request.Params["CustomerId"];

        if( !string.IsNullOrEmpty(customerId) )
        {
          appVirtualDir = string.Concat(appVirtualDir, '/', customerId);
        }
      }

      return base.InternalBuildUrl(area, controller, action, protocol, port, domain, subdomain, appVirtualDir, 
        extension, absolutePath, applySubdomain, suffix);
    }
  }
}

Now, how do I instruct MonoRail to use my UrlBuilder instead of the DefaultUrlBuilder?  Once again, it is a simple configuration change.  Based on information from the MonoRail Configuration reference, its a piece of cake to slip in a custom service.  The configuration below goes in the MonoRail section of your web.config.  Using the well known id 'UrlBuilder' and the full type of my class, we are rocking and rolling.

<services>
  <service id="UrlBuilder" type="My.Product.Mvc.AppendCustomerIdUrlBuilder, My.Product.Core" />
</services>

posted @ Thursday, June 21, 2007 10:29 AM


Print

Comments on this entry:

# re: MonoRail - URL Rewriting and Persisting QueryString Parameters

Left by Diego Guidi at 9/7/2007 5:44 AM

I'm playing with routing using your suggests, but the code not works in my system... maybe routing requires IIS ir, as i think, works also with Cassini?

# re: MonoRail - URL Rewriting and Persisting QueryString Parameters

Left by Diego Guidi at 9/7/2007 6:13 AM

Ok it works!
I've made some mistakes with regexp's

Your comment:



 (will not be displayed)


 
 
 
Please add 4 and 8 and type the answer here:
 

Live Comment Preview:

 
«May»
SunMonTueWedThuFriSat
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567