Ayende @ Rahien
Technology, life, .Net and all the rest...

Brail's architecture

First, let's understand where Brail lives. Brail is a View Engine for the Castle's MonoRail web development framework MonoRail is MVC framework for ASP.Net that allows true Separation Of Concerns between your business logic and your UI code. Brail come into play when it's time to write your UI code, the idea is that instead of using a templating framework, like NVelocity or StringTemplate, you can use a bona fide programming language with all the benefits that this implies. The down side of this approach is that programming languages usually has very strict rules about how you can write code and that is usually the exact opposite of what you want when you write a web page. You want a language that wouldn't get in your way. This is where Brail come into play.

Brail is based on Boo, a programming language for the .Net Framework which has a very unorthodox view of the place of the user in the language design. Boo allows you to plug your own steps into the compiler pipeline, rip out whatever you don't like, put in things that you want, etc. This means that it pack a pretty punch when you needs it. The options are nearly limitless. But enough raving about Boo, it's Brail that you're interested in. What Brail does is to allow you to write your web pages in Boo, in a very relaxed and free way. After you write the code, Brail takes over and transform it to allow you to run this code. The Brail's syntax and options is documented, so I'll assume that you're already familiar with it.

Question: How Brail does all of it?

Answer: It's magic.

Pay no attention to the man behind the curtain:

 So, what is really happening there? First we need to understand what MonoRail is doing when it receives a request, here is the pretty diagram:

Processing Requests:

MonoRail receive a request, calls the appropriate controller and then calls to the view engine with the current context, the controller and the view that needs to be displayed. Brail then takes over and does the following:

  • Check if the controller has defined a layout. If it has, pipe the view's output through the layout's output. (The view is compiled the same way a view is)

  • Get the compiled version of a view script by:

    • Checking if the script is already in cache. The cache is a hash table ["Full file name of view" : compiled type of the view]
    • If the script is already in the cache but the type is null this means that the view has changed, so we compile just this script again.
    • Instantiate the type and run the instance, which will send the script output to the user.

A few things about changes in the views. Brail currently allows instantaneous replacement of views, layout and common scripts by watching over the Views directory and recompiling the file when necessary, since this is a developer only feature, I'm not worrying too much about efficiency / memory. I'm just invalidating the cache entry or recompiles the common scripts. Be aware that making a change to the Common Scripts would invalidate all the compiled views & layouts in memory and they would all have to be compiled again. This is done since you can't replace an assembly reference in memory.

The interesting stuff is most happening when Brail is compiling a script. For reference, Brail usually will try to compile all the scripts in a directory (but does not recurse to the child directories) in one go, since this is more efficient in regard to speed / memory issues. Occasionally it will compile a script by itself, usually when it has been changed after its directory has been compiled or if the default configuration has been changed. There isn't much difference between compiling a single file and compiling a bunch of them, so I'm just going to ignore that right now and concentrate on compiling a single script. Brail's scripts are actually a Boo file that is being transformed by custom steps that plug into the Boo compiler.

Compiling Scripts:

Here is what happens when Brail needs to compile a script:

  • Creating an instance of BooCompiler, and telling if to compile to memory or to file (configuration option).

  • Adding a reference to the following assemblies: Brail, Castle.MonoRail.Framework, the compiled Common Script assembly and any assembly that the user referenced in the configuration file.

  • Run a very simple pre processor on the file, to convert file with <% %> to a valid boo script.

  • Remove the default Boo's namespace (this is done because common names such as list, date were introduced including the default namespace and that meant that you couldn't use that as a parameter to the view.

  • Replace any variable reference that has unknown source with a call to GetParameter(variableName) which would use the controller's PropertyBag to get it. GetParameter() throws if it can't find a valid parameter, by the way. The reasoning is that this way you won't get null reference exceptions if you are trying to do something like: date.ToString("dd/mm/yyyy") and the controller didn't pass the date. Since debugging scripts is a pain, this gives you a much clearer message.

  • Then the real transformation begins. Any Brail script is turned into a subclass of the BrailBase class, which provides basic services to the script and allow the engine to output the results to the user. What is happening is that any "free" code, code that isn't in a class / method is moved to a Run() method on the subclass. Any methods are moved to the subclass, so they are accessible from the Run() method. Anything else is simply compiled normally.

When Brail receive a request for a view it looks it up as describe above (from cache/compiled, etc). A new instance of the view is created and its Run() method is called. All the output from the script is sent to the user (directly or via the layout wrapping it.)

BrailBase Class:

The BrailBase class has several useful method & properties:

  • ChildOutput - Layouts are scripts that are using their ChildOutput property to wrap their output around the child output. This works as follows, a layout is created, and its ChildOutput is set to a view's output, the view is then run. After the view run, the layout is run and has access to the view's layout.

  • IsDefined(parameterName) - Check if a parameter has been passed, this allows you to bypass GetParameter() throwing if nothing has been passed.

  • OutputSubView() - Output another view.

To wrap it up:

As you can see, the man behind the curtain does quite a bit, but it's really a lot of very small & simple steps. If you've any questions, just email / IM me.