This is a follow up to my previous post about running the new ASP.Net MVC framework on top of Mono.
Firstly, you'll need CTP 2 of the ASP.Net MVC framework, which you can get here. I'm going to assume that you know enough about ASP.Net to make the changes I describe. If you need more information about the MVC framework http://asp.net/mvc/ has some excellent information on it.
Secondly, you'll need to be running an up-to-date version of Mono. At this time, the last official release is 1.2.6, which doesn't have all of the required dependencies. I am running preview 4 of Mono 1.9. The latest preview can be found here.
Thirdly, for this to work your application can't depend upon anything that's not supported in Mono. I had to remove some references to System.Web.Extensions, System.Data.DataSetExtensions, and System.Data.Linq from my Web.config file in order for this to work. I would reccomend removing those references from your project as well.
<compilation debug="false">
<assemblies>
<add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<!--<add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>-->
<add assembly="System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add assembly="System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<!--<add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>-->
<!--<add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>-->
<!--<add assembly="System.Data.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />-->
</assemblies>
</compilation>
Now, down to business.
Mono's HttpApplication Implementation
The System.Web.Mvc assembly depends upon the System.Web.Routing assembly for (you guessed it) routing. Obviously, this means that System.Web.Routing needs to work with Mono if we are to even have hope of getting System.Web.Mvc to run on it. In order to take advantage of the routing capabilities of System.Web.Routing, you register the System.Web.Routing.UrlRoutingModule class with the ASP.Net runtime. UrlRoutingModule takes incoming requests and routes them to the appropriate IHttpHandler implementation. If you try to run the MVC sample app on Mono unmodified, UrlRoutingModule won't work and you'll get lots of 404's.
I tried to write my own routing module, but I became frustrated when I couldn't get it working after about half an hour. After digging a little deeper and walking walking through Mono's HTTP request pipeline, I found an inconsistency between Mono's HttpApplication implementation and the one that ships with the .Net Framework. The .Net implemenation allows you to use HttpContext.Handler from an IHttpModule to set which IHttpHandler will be used to process a request. The Mono implementation does not; it seems to always resolve the handler based on the extension of the requested URL. UrlRoutingModule is apparently using HttpContext.Handler to specify which IHttpHandler should handle the request, which means that it doesn't work with Mono. This is also the reason my custom routing module wouldn't work.
All is not lost, however. We can override Mono's default IHttpHandler resolution using an IHttpHandlerFactory. This means, however that our factory has to do all of the routing work that UrlRoutingModule would have done for us. It's actually not that much code though.
public class MvcHttpHandlerFactory : IHttpHandlerFactory {
public IHttpHandler GetHandler(HttpContext httpContext, string requestType, string url, string pathTranslated) {
var context = new HttpContextWrapper2(httpContext);
using(RouteTable.Routes.GetReadLock()) {
var routeData = RouteTable.Routes.GetRouteData(context);
if (routeData == null) {
throw new InvalidOperationException("Invalid route data.");
}
var routeHandler = routeData.RouteHandler;
if (routeHandler == null) {
throw new InvalidOperationException("Invalid route handler.");
}
var requestContext = new RequestContext(context, routeData);
return routeHandler.GetHttpHandler(requestContext);
}
}
public void ReleaseHandler(IHttpHandler handler) {
if (handler is IDisposable) {
((IDisposable)handler).Dispose();
}
}
}
<httpHandlers>
<!-- snip -->
<add verb="*" path="*.mvc*" type="MvcOnMono.MvcHttpHandlerFactory, MvcOnMono"/>
</httpHandlers>
One side-effect of this is that we have to adjust the routes for our controllers to use some kind of extension (I'm using ".mvc"). We have to map our IHttpHandlerFactory to a set of paths, and it can't be *.* because then Mono doesn't handle static files correctly. Another side-effect is that /Default.aspx will no longer map to the Home controller, since our factory doesn't perform routing for .aspx files. Using an IHttpModule would remove these limitations, but as we already saw that's not currenlty possible with Mono.
Here's what the routes should look like in order to run the sample application:
public static void RegisterRoutes(RouteCollection routes) {
// Note: Change the URL to "{controller}.mvc/{action}/{id}" to enable
// automatic support on IIS6 and IIS7 classic mode
routes.Add(new Route("{controller}.mvc/{action}/{id}", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(new { action = "Index", id = "" }),
});
routes.Add(new Route("Default.aspx", new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});
}
Now, I have no idea if this is the right way to route the requests using System.Web.Routing, since there's not really any documentation for it yet. It seems to work fine for now though. Please let me know if you spot any problems :)
You should be able to run the sample app on .Net at this point by browsing to "/Home.mvc/". It won't work on Mono just yet, but if it doesn't work on .Net then you probably need to fix something before it will work on Mono.
Mono's DefaultVirtualPathProvider
At this point, if you try to run the appliction on Mono's XSP2 web server, you will get the following error.
After a quick look at the Mono source, I disovered the following in System.Web.Hosting.DefaultVirtualPathProvider:
if (UrlUtils.IsRelativeUrl (virtualPath))
throw new ArgumentException (String.Concat ("The relative virtual path '", virtualPath, "', is not allowed here."));
I'm not sure why this is in there, but it's not that hard to circumvent. We just need to create our own VirtualPathProvider that can actually deal with application-relative paths, and register it with the System.Web.Hosting.HostingEnvironment class.
public class MvcVirtualPathProvider : VirtualPathProvider {
public override bool FileExists(string virtualPath) {
if (VirtualPathUtility.IsAppRelative(virtualPath)) {
var physical = HostingEnvironment.MapPath(virtualPath);
if (File.Exists(physical))
return true;
}
return Previous != null && Previous.FileExists(virtualPath);
}
}
protected void Application_Start(object sender, EventArgs e) {
RegisterRoutes(RouteTable.Routes);
HostingEnvironment.RegisterVirtualPathProvider(new MvcVirtualPathProvider());
}
That's it! That's all you have to do! You should now be able to browse to "/Home.mvc" in the sample app while running Mon's XSP2 web server and it should run just fine.
If you want the code, you can download the Mono-ready MVC sample application here.
Conclusion
This really isn't that much code. As far as I can tell, all the Mono team would need to do to make MVC work without any tweaking is fix their HttpApplication implementation and change their default VirtualPathProvider to correctly deal with app-relative paths.
Something I noticed while working on this was that when I clicked "About" in the MVC sample app the site's CSS file would fail to load. Apparently, putting the ".mvc" extension on the controller part of the route causes the .Net implementation to do something funky with the CSS file's virtual path. It seems to work fine in the Mono implementation though. I'm not sure which is correct, but Mono behaved the way I was expecting, while the .Net Framework crapped out.
I haven't tested this on Linux with mod_mono yet, so if you end up doing so please let everyone know if it works. I don't see any reason it shouldn't, but I might be missing something.