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>

MonoRail - Runtime Views and Layouts with Filters

I've been on a bit of a MonoRail kick lately and thought I might share some gems.  In his recent post Hammett talks about, among other things, using a filter to change layouts at runtime.  He didn't elaborate so I thought I would share the way I've implemented his idea.  In our hosted environment we generally prefer each customer to have a separate host header.  For various reasons this isn't possible for some customers.  Instead, we identify them based on an internal id.  We key everything about the application off of this id.  We use this id to display a customized user interface and run custom logic on a per customer basis.  The way I am currently implementing the customized user interface is, as Hammett stated, with Filter. 

We grab the CustomerId from the query string and use that to determine if that particular customer has a custom view using a simple folder structure: Views -> CustomerId -> Controller -> View.brail.  If no custom view is found, we instead use a 'base' view that is stored in an identical structure Views -> Base -> Controller -> View.brail.  Pretty straight forward.  We do the same thing for layouts.  Decorate your base controller with an AfterAction attribute and you are all set.

namespace My.Product.Mvc
{
  using System.IO;
  using Castle.MonoRail.Framework;

  public class CustomViewFilter : IFilter
  {
    public const string BASE = "Base";
    public const string LAYOUTS = "Layouts";

    #region IFilter Members

    public bool Perform(ExecuteEnum exec, IRailsEngineContext context, Controller controller)
    {
      string customerId = context.Params["CustomerId"];
      return Perform(exec, context, controller, customerId);
    }

    #endregion

    public bool Perform(ExecuteEnum exec, IRailsEngineContext context, Controller controller, string customerId)
    {
      // Use the Base or Customer specific view
      string viewName = null;
      if( null != controller.SelectedViewName )
      {
        viewName = Path.Combine(BASE, controller.SelectedViewName);
        if( null != customerId )
        {
          string customerViewName = Path.Combine(customerId, controller.SelectedViewName);
          if( controller.HasTemplate(customerViewName) )
          {
            viewName = customerViewName;
          }
        }
      }
      controller.SelectedViewName = viewName;

      // Use the Base or Customer specific layout
      string layoutName = null;
      if( null != controller.LayoutName )
      {
        layoutName = Path.Combine(BASE, controller.LayoutName);
        if( null != customerId )
        {
          string customerLayoutName = Path.Combine(customerId, controller.LayoutName);
          string customerLayoutPath = Path.Combine(LAYOUTS, customerLayoutName);
          if (controller.HasTemplate(customerLayoutPath))
          {
            layoutName = customerLayoutName;
          }
        }
      }
      controller.LayoutName = layoutName;

      return true;
    }
  }
}
«June»
SunMonTueWedThuFriSat
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567