Umbraco on Mono

This week I've spent some time trying to fix bugs preventing Umbraco from running unmodified on Mono. Unfortunately, some modifications to Umbraco source code (diff here) were necessary due to the inherent Windows-centric coding in parts of the CMS. The diff above takes care of just one part of the cross-platform issues in Umbraco, namely it replaces hard-coded use of the \ directory separator character with the, correct, Path.DirectorySeparatorChar. For some reason MONO_IOMAP mechanism didn't take care of the problem, hence the workaround (I'm going to fix the IOMAP issue after I'm back from vacation mid-August). The other cross-platform issue is (very common in Windows .NET applications) inconsistent use of file naming case which MONO_IOMAP is dealing with nicely.

Making Umbraco run on Mono required some fixes to our ASP.NET parser (revisions 138474, 138520, 138521 and 138592 in trunk, for those interested. Backported to both 2.4 and 2.4.2 branches) as well as making AppDomain not reload when Umbraco writes to the Web.config file. All of the fixes willbe part of the upcoming 2.4.2.3 release. There are however some issues not fixed yet, but with workarounds:

Web.config modifications during setup
Generally it works fine - application isn't restarted, data is written to the config file, but for some reason Umbraco installer caches data from before database access credentials change. So if, for instance, you were presented with MS-SQL setup box and switched to MySQL, the connection and/or database creation will fail trying to use MS-SQL code. This is a bug in Mono which I'm going to investigate in August. Workaround for this issue is to stop the app after selecting the new database provider and credentials and just restart the installation process - everything will work fine from that moment on.
VistaDB not working
VistaDB comes without source, so I wasn't able to investigate what the issue was. The symptom is that when trying to connect to or populate VistaDB database you will get either a ThreadAbortException, a TypeInitializationException or a JIT error about invalid IL in the VistaDB assembly. This is likely a runtime issue - to be investigated. You can use either SQL Server or MySQL providers instead (I used MySQL for testing).
Intermittent exceptions
Sometimes visiting certain pages in the admin interface causes an exception to be thrown (usually NullReferenceException) but I never succeeded in reproducing the exception(s) by repeating the actions. To be investigated.
TinyDLL.dll is a native library
Since it is native, our compiler cannot figure out what to do with it and throws an exception. Simply remove the dll from there - it shouldn't be there anyway.
So, this is the current status of Umbraco on Mono - please play with it and report any bugs you might find, giving exact directions how to reproduce the issue. I will take a look at them once I'm back from vacations.

Since not everybody might want to or know how to compile and set up Umbraco from sources, I have precompiled the package with the diff applied and repackaged the 4.0.2.1 distribution with the bin/ assemblies replaced by their "Mono versions" (note that the ONLY difference in the assemblies is that the patched ones are cross-platform). Here are the download locations:

Mono ASP.NET MVC saga update

As many of you are probably well aware, we failed to ship the recently open-sourced Microsoft ASP.NET MVC with Mono 2.4.2. The release was soon followed by an update, 2.4.2.1, which included the source of MVC but, unfortunately, the story doesn't end here.

Sometime during MVC integration process, we discovered our C# compiler had a bug which made certain parts of MVC behave incorrectly. The bug was quickly fixed in trunk, but it was determined that backporting it to the 2.4 branches was not an option and since the issue was easily worked around, we decided to just put the modified MVC code in our repositories. Unfortunately, due to my mistake, the workaround didn't actually make it in neither 2.4.2.1 or 2.4.2.2 releases. I have just committed the 2-line fix (revisions r138087 and r138088) to the 2.4 branches and we should expect to see 2.4.2.3 released soon. My apologies for this omission to everybody who expected to see the bug fixed in 2.4.2.2.

If your MVC application throws an exception similar to this one:

System.ArgumentNullException: Argument cannot be null.
Parameter name: type
  at System.ComponentModel.TypeDescriptor.GetConverter (System.Type type) [0x000ab] in /usr/src/tmp/mono-branch/mcs/class/System/System.ComponentModel/TypeDescriptor.cs:437 
  at System.Web.Mvc.DefaultModelBinder.BindModel (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext) [0x000ba] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/DefaultModelBinder.cs:186 
  at System.Web.Mvc.ControllerActionInvoker.GetParameterValue (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ParameterDescriptor parameterDescriptor) [0x00090] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/ControllerActionInvoker.cs:119 
  at System.Web.Mvc.ControllerActionInvoker.GetParameterValues (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ActionDescriptor actionDescriptor) [0x00021] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/ControllerActionInvoker.cs:128 
  at System.Web.Mvc.ControllerActionInvoker.InvokeAction (System.Web.Mvc.ControllerContext controllerContext, System.String actionName) [0x00099] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/ControllerActionInvoker.cs:162 
then you are hit by the bug. At this point you have three choices:
  • Download the fixed binary from here, unpack it and replace the copy found in your 2.4 GAC either manually or by issuing the following command as user who has GAC folder write rights:
    gacutil -i System.Web.Mvc.dll
  • Take System.Web.Mvc sources from the Mono 2.4 (or 2.4.2) branch, compile it (with 2.4+ compiler)
  • Use System.Web.Mvc.dll from Microsoft .NET distribution - it will work just fine with Mono

ASP.NET MVC sources in the Mono 2.4 and 2.4.2 branches are identical to the upstream source except for the workaround. Trunk version of ASP.NET MVC is identical to upstream.

Mono with CGI on shared hosts

Some of you might want to run Mono on a shared (virtual) Linux host but your ISP/hosting company doesn't let you install mod_mono or Apache FastCGI modules. Kornél Pál has just contributed a piece of documentation which explains how to host Mono applications on CGI-only boxes without incurring the high CGI cost.

System.Web.Routing URL matching and generation

Recently .NET got a new feature - System.Web.Routing - which is a general purpose routing engine used at this point (within the realms of .NET class library) by System.Web.Mvc and System.Web.DynamicData namespaces. Most of System.Web.Routing is pretty straight-forward and easy to understand, but there's one pretty poorly documented feature (albeit a very important one, not to say crucial) - the route URLs.

During the past week was fixing several bugs in the Mono implementation of System.Web.Routing engine, all of which were related to route URL parsing and virtual path generation. At the beginning of the week I thought the task would be quick and easy, but it turned out that documentation of some aspects of the subject is few and far between and, in effect, correct implementation of the support took me the whole week. Since the feature is important for both the implementor of the namespace as well as for the end user, this blog entry attempts to collect and explain all the details of the subject.

URLs are used for two purposes in System.Web.Routing - for checking if a request matches a route (in this instance the route URL is considered to be specified using a simple pattern DSL) and for generating virtual paths (in this instance the route URL is considered to be a replacement pattern for the virtual path).

Note: all of the information below has been acquired through tests and whatever little documentation is available on the subject, I saw no source code for .NET's version (we never do that at Mono, as you probably know) so there might be errors and mistakes in the description. If you find one, please let me know (or better yet - create a simple test case which demonstrates the failure and file a bug for Mono's Sys.Web component. If you're interested in the tests we have, feel free to take a look

From the two, route matching is better documented, but let me outline the rules which the matching URLs follow:

  • URL must not start with either ~ or / characters
  • URL must not contain the ? character.
  • Each URL is built from zero or more segments separated with the / character
  • Each segment may contain any number of sections enclosed in the { and } characters.
  • Within one segment, no two sections can be adjacent, they must be separated with literal string of characters.
  • No section name must be empty. Empty in this case is understood as "not containing any characters" - you can have a section name consisting only of whitespace (including tabs and newlines).
  • Sections are parsed using hungry matching - that is, they will consume all the text from their starting point all the way to the start of the last ocurrence of the following literal or to the end of input, whichever comes first.
  • A segment may contain a catch-all section which is marked by putting * as the first character of the section name. Catch-all sections must come alone within the segment and they are valid only in the last segment of the URL

This is a pretty simple set of rules and is easy to implement (see PatternParser.cs in Mono repository)

 

Let's pass to the other part of the subject, virtual path generation. The theory seems simple here - take the original route URL and replace it with provided values. The simple recipe has embedded traps, however. Once you discover the rules, they're easy and the algorithm is clear, but discovering them is, as it turns out, not so easy today. Thanks to Atushi Enomoto's work on implementing our System.Web.Routing there already was basic support for the parsing/matching/generating operations which covered the most common cases. It didn't work correctly for the more obscure and less frequently encountered scenarios. There was very little meaningful information on the web I could find on those and, by far, the most valuable piece of information came from the "ASP.NET MVC 1.0" book (Chapter 4). All gathered together, however, it still didn't cover everything. What you can see below is a conglomerate of the above information with knowledge learned from tests.

There are several terms you need to be familiar with in order to understand what's going on:

Required values
Values which are found in the route URL (i.e. sections) which don't have defaults provided
Default values
Values provided by the programmer in the route's Default dictionary.
Ambient values
Values gathered by the routing engine from the current request. They are accessible via RequestContext.RouteData.Values
Constraints
Programmer can provide the routing engine with a set of per-route constraints. Constraints may be presented in two flavors - either strings, which are treated as regular expressions, or instances of the IRouteConstraint interface. Those values are consulted when generating the virtual path.
Overflow values
Those are the values provided explicitly in the call to Route.GetVirtualPath that are not in the route URL and also are not specified in the Defaults and Constraints dictionaries attached to the Route class. Such values are appended as query parameters to the generated virtual path.

Given the information above, the rules for virtual path generation are outlined below. This is where it becomes a bit entangled.

  • If there are any required parameters in the current route, a check is made whether all of them are present in the dictionary passed by user to the GetVirtualPath method.
    • If any are missing, there are several rules before we decide it's a mismatch:
      • If there are no defaults
      • or the defaults dictionary contains at least one value used in the route URL
      • and the user values do not contain any value used in the route URL,
      • then the code is allowed to check for the required value in the ambient values. If the value is not found there, we have a mismatch.
      • If the above four conditions are not met and the value is not found in the user values, we have a mismatch
    • If all of them are present, checks are performed to see whether
      • there are any default values which are not used in the route URL.
      • If any such entries are found, another check is made to make sure that any corresponding values found in the user-passed dictionary have exactly the same value as specified in the defaults. If the check fails, we have a mismatch.
      • Further on, the code looks whether there are any constraints defined for the route and, if yes, iterates over the dictionary to make sure that all of the constraints are met.
      • If there are no errors, we have a match and we can start generating the virtual path.

This completes the preliminary checks whether the route qualifies for virtual path generation in the context of the current request.

 

The actual virtual path code generation can begin now. Code generating the virtual path can trim the result if by skipping continuous block of values at the end of the URL if all of the values are defaults. After this process is done, code looks for the overflow values and appends them to the resulting virtual path as query parameters. Both names and values in the query are encoded using Uri.EscapeDataString.

This concludes the virtual path generation process. Hope somebody finds this description useful :)

Book meme, per instructions :)

Following a fellow hackerette's instructions, here's my meme:

"Computer science has a subdiscipline called Information Retrieval 
(IR for short) that focuses almost entirely on this problem."

And the original Andreia's instructions copied here:

  • Grab the nearest book.
  • Open it to page 56.
  • Find the fifth sentence.
  • Post the text of the sentence in your journal along with these instructions.
  • Don't dig for your favorite book, the cool book, or the intellectual one: pick the CLOSEST.

So, Andreia, what's the page number for next week? :)