Extending and customizing MonoRail

June 14th, 2007

Recently I’ve built a CMS like infrastructure on top of MonoRail. Much of what I’ve used is described in a earlier post about Portlets, but that covers only one aspect of what has been built and how. I’ve asked my client to release what has been done back to the Open Source community, but I don’t think this is going to happen. Obviously they hold the IP of what has been created, so I cannot share anything. What I’m going to try here, however, is to show where are MonoRail’s extensibility points — at least some of them — and how to use them.

As you know my writing style is a clear-cut, show me the code but let me know the whys and where, with a pinch of poor english. I’m going to try to list the main challenges in building a extensible platform with MonoRail.

The scenario

Suppose you want to develop a general purpose CMS app, much like Cuyahoga. As any decent CMS, adding and changing content must be awfully si\mple, but it cannot stop there. It must be extensible. Pre-built packages of extensions may come with it, but you may also want to expose the API so anyone can develop extensions (think blog, forum, product catalog, shopping basket).

For an example on how to do that using WebForms refer to Cuyahoga itself or dotnetnuke. I’ll not comment on how easier or harder it is to do with MonoRail, leaving that judgment to you.

The core

At this point you have to decide if your app is going to be purely driven by extensions (let’s call it modules from now on) or it will be a core set of features that can eventually be extended by the modules. This is a significant decision with some complex impacts. For example, if the app targets an specific market say, CRM for gadgets sellers, you will have to offer extension points on your API as well.

Let’s just assume that your app is just a host for modules. Even that bring some complexity to the table. How much orthogonal will it be? In other words, how much a module can impact on loaded modules? And how much a module can change the behavior of existing modules on purpose? Being concrete, we could create a single captcha module that affects all other modules that post relevant data back to the app.

Before we get buried into the complexity and the consequences of every path, let’s just assume that it must be orthogonal, with no intentional or unintentional side effects.

MonoRail’s Services

MonoRail is made of a set of services and their default implementation. From those I’d emphasize a few:

  • IControllerTree: it’s a registry of controllers, sorted by the areas
  • IUrlTokenizer: breaks the url requested into pieces to extract the area, controller name and action
  • IUrlBuilder: used to output a url. All standard helpers rely indirectly on it
  • IViewSourceLoader: used by view engines to access the view template stream. This layer of abstraction allows us to sometimes use the filesystem, and sometimes assemblies resources

These services are initialized by that module you add to your web.config and always wonder “what heck does that do?”. Yes, the EngineContextModule. It does more than that, though. But it’s important that you know that this module — and any other ASP.Net module — is only loaded after the Application_OnStart event is fired. This might makes sense, but might make our life a little more miserable. I’ll show why soon.

Adding controllers and views in runtime

Before imagining and playing with the concept of modules, it’s a good thing to learn how we can add controllers and its views in runtime. IControllerTree is the one you’d use to register controllers, and IViewSourceLoader to include views.

Something like:

IControllerTree tree = 
    ServiceContainerAccessor.ServiceContainer.GetService(typeof(IControllerTree));

IViewSourceLoader loader = 
    ServiceContainerAccessor.ServiceContainer.GetService(typeof(IViewSourceLoader));

// Area: empty, controller: home
// url: http://host:port/virtualdir/home/actionname.castle
tree.AddController(string.Empty, "Home", typeof(MyCoolController)); 

// Area: CMS/Admin, controller: Emails
// url: http://host:port/virtualdir/CMS/Admin/Emails/actionname.castle
tree.AddController("CMS/Admin", "Emails", typeof(EmailManagerController)); 

loader.AddAssemblySource(new AssemblySourceInfo("AssemblyName", "NamespaceThatPreceedsViewFolder"));

You can also use the controller tree to “replicate” a controller, and let it handle more than a single url. Another thing I need to mention is that when you’re using Windsor integration, you just need to add a new controller to the container, and it will be added to the controller tree automatically (by the RailsFacility).

But that is not the difficult part, it’s “when” to run such code. But we will get back to it.

Handling unanticipated actions

By default actions are instance methods on your controller classes. You can also use dynamic actions. But another approach is to use a default action.

A default action is a method that will be executed whenever an action cannot be mapped to an instance method on your controller. This is how you use it:

[DefaultAction("Whoops")]
public class MySmartController : Controller
{
    public void Whoops()
    {
       // I'm going to be executed if the user types /mysmart/foobar.castle
       // You can use the property Action to see exactly what has been typed on the URL (regarding the action) 
    }
}

A different folder structure is possible

You don’t need to be restricted to the folder default structure. You can organize views in different folders, in a way that makes sense for you project. The only restriction is that there must be a single common root folder for the views (usually the folder is called “Views” too).

To make the controller render a different view, in a different location, use RenderSharedView, something like:

public class HomeController : Controller
{
    public void Index()
    {
       RenderSharedView("Clients/Something/SomethingElse/index"); 
    }
}

Changing actions and layouts in runtime

The view engine uses the controller’s SelectedViewName property to resolve the view it needs to render. And this property is writable. So it’s not difficult to perform some logic to render different views, or views in different places. I usually do this with a filter.

For Layouts, the same idea applies. The property LayoutName defines the layout to be used. There’s a little trick if you do not want to use the default “layouts” folder, though. When specifying the new path use “/” as the first character.

Putting it all together

Well, not all, but some. I’ve created a sample application that illustrates some of the above topics. It’s an example CMS app that does not have anything of CMS, aside from the name. It illustrates how to get modules working — using Windsor –, controllers being added by modules, views and default actions.

Remember what I said regarding “where to put the code”? Well, I’m using the first BeginRequest to start up all the modules. Looks really odd, and I’d definitely appreciated a better idea:

public GlobalApplication()
{
    BeginRequest += new EventHandler(OnFirstRequest);
}

[MethodImpl(MethodImplOptions.Synchronized)]
private void OnFirstRequest(object sender, EventArgs e)
{
    // This is sample code, so the locking strategy here is, well, a lazy one. 
    // Please implement something more robust for a real project

    if (modulesLoaded) return;

    if (!modulesLoaded)
    {
        modulesLoaded = true;
    }

    ModuleInstallerManager manager = container.Resolve();

    manager.InstallModules();

    BeginRequest -= new EventHandler(OnFirstRequest);
}

Download the sample code.

As usual, comments are welcome.

14 Responses to “Extending and customizing MonoRail”

Chris Ortman Says:

Yes this is a complex problem. It was for the concept of modules that I implemented passing variables to NVelocity’s #component directive.

I don’t think I understand your issue with when to load the modules…why not a facility that inspects types from module assemblies?

That single root folder for views has also been a problem for me, in order to have “themes” i need to have customer specific layout / css / image files. All of which go into my Content/Themes/ThemeName folder. I then have to monitor theme folders for changes to *.vm files and copy to the layouts folder when needed. I thought about placing the content files in the view folder, but it just didn’t feel right.

Then there’s the security issues…needing to control access to specific controllers and actions. For now it’s just a table that maps a right to a url, so far it works but I’ve not been asked to support very complicated security settings yet.

hammett Says:

Chris, about loading modules: the issue is that it depends on MonoRail services. If I do what you’re saying the ServiceProviderAccessor will return me a null container as MonoRail http module is only created after Application_OnStart.

We also have implemented support for themes. The layouts and views related to a theme lies on the Views/themes/themename folder. But the static content for it lie elsewhere. Skins were also supported.

Security was not implemented on our solution yet.

Chris Ortman Says:

What if you put your ServiceProviderAccessor into Windsor and load your modules in the Component_Added?

The nice thing about having my themes all in one folder is easy deployment and version control…not that it would be terrible hard to write something to accept a zip file and extract to necessary directories. Views/themes/themename is pretty good as well, I imagine I’ll have to do something similar when I get a request to theme a page beyond what I can do with CSS.

hammett Says:

But Chris, those services belongs to MonoRail. The root of the problem is that they are only accessible after the container is created. ComponentAdded will run before.

I see your point about the themes, but I can say: the views folder is more a limitation on NVelocity. What we can do is set the root web project as the view folder. Have you tried that?

Colin Ramsay Says:

Heh, the reason I sent you a patch to expose SelectedViewName so long ago was to do some really cool stuff with themes and changing the layout and view at runtime in a project I was working on at the time. That one change enables a host of possibilities, because the system was designed to be multisite, with each site having a completely different theme.

The rest of your post has provided the solution to something I’ve been pondering for a while. I have an ASP.NET CMS which is modular and works from UserControls which often postback to each other. By allowing the modules to add controllers at runtime I can actually provide a more robust experience than with the previous setup.

Awesome.

Dirk Maegh Says:

I think Monorail would excel as well in a client only application, which shows web pages as its user interface.

I just don’t know how to replace the httpmodule with the new “connecting the dots” working horse.

Would you have an idea on how to start this kind of usage ? I know it has not specifically been built for this purpose, but I think it would shine !

hammett Says:

Dirk, the simplest approach would be to embed cassini in a winforms app and have it browsing the local web server.

Niklas Says:

This is just what I need. I’ve got the module concept working with the controllers, but it doesn’t seem to find the views in the module assembly. I’ve downloaded and duplicated your code (using IViewSourceLoader’s AddAssemblySource to add the views in the IModuleInstaller implementation, etc) and am simply trying to externalize my HomeController (as a test). It throws “RailsException: Could not find view template: home\index”. Any pointers?

hammett Says:

Niklas, make sure you mark the files as embedded resources.

Niklas Says:

Ahh, thank you for that final piece of the puzzle.

Cao Says:

Can you give more detailed explanation on Themes and Skins? further,any samples?

Bergius Says:

I’m using this concept, and it’s working nicely. However, I’d like to also load filters (that take parameters) from modules… Is there any existing solution for this, or do I need to figure it out on my own?

For example, an auth module would contain a controller for logging in and out and perhaps one for managing roles, view components to insert authentication elements in the GUI and filters to enforce authentication and authorization, which can be used by modules that know only that they can expect some kind of interface or a component key… Does that make any sense? :)

Cristi Says:

Nice idea! Unfortunately not work with last version from trunk. (I can’t find ServiceContainerAccessor)

Martin Hemmer Says:

one more nice topic in your blog and nice comments too keep it up, If you advise some more related links to topic. I’m very interested in CMS and all its related subjects.

Leave a Reply