Introducing MonoRail.HotSwap

time to read 4 min | 662 words

I hate aimless bitching, and this post annoyed me enough to decide to do something about it. The basic problem is that making any change at all to an ASP.Net application requires an AppDomain load / unload, which takes a lot of time. This means that a quick change and browser refresh are not possible, you are trying to minimize those waits as much as possible, and that hurts the feedback cycle.

MonoRail makes it much easier, because you are not forced to do this if you make any change to the views, but changing the controller can get to be a PITA, because of the wait times.

Problem.

Let me walk you through what I was thinking:

  • I need to have a way to get the new version of the controller into the already running application.
  • I am using Windsor.
  • I had a discussion two weeks ago about Java's hot deployment capabilities.
  • Controllers are independent units, for the most part.

So, what does this mean? It means that I can probably take any single controller and recompile it independently of the rest of the application. As a direct result of that, I can register the new controller version in Windsor, so the next time MonoRail needs this controller, it will get the new version.

The rest was just a matter of runtime compilation...

The code is listed below, I was astonished to discover that this is so freaking easy!

The proof of concept code is 70 lines, and it allows to make any change to a controller and it will be immediately be reflected in the application.

Now, why is it proof of concept?

  • It doesn't really handles errors correctly, it just removes the controller if it was changed and that is it. You probably want to get an error page with the compilation errors.
  • It will recompile the minute that you are saving, you probably want to recompile on the first request.
  • You may want to recompile the entire application if the first compile did not succeed, that would allow you to handle more than a single change. That is fairly complex thing to do, though.

The first two issues can be fairly easily solved using a SubDependencyResolver on Windsor, which will intercept the incoming calls for controllers' instances. It would then try to compile the controller if it was changed, and throw an exception if it cannot compile.

Important: I am going to leave to JAOO in a few hours, so I won't have time to follow up on this as it deserves. I have every confidence that William will :-)

Here is the code:

namespace Castle.MonoRail.Framework
{
	using System;
	using System.CodeDom.Compiler;
	using System.IO;
	using System.Reflection;
	using Microsoft.CSharp;
	using Windsor;

	public class HotSwap
	{
		private readonly string directoryToWatch;
		private readonly IWindsorContainer container;
		private readonly Assembly assembly;
		private readonly string controllersNamespace;

		public HotSwap(string directoryToWatch, IWindsorContainer container, 
			Assembly assembly,
			string controllersNamespace)
		{
			this.directoryToWatch = directoryToWatch;
			this.container = container;
			this.assembly = assembly;
			this.controllersNamespace = controllersNamespace;
		}

		public void Start()
		{
			FileSystemWatcher watcher = new FileSystemWatcher(directoryToWatch, "*.cs");
			watcher.Created += CodeChanged;
			watcher.Changed += CodeChanged;
			watcher.Renamed += CodeChanged;
			watcher.EnableRaisingEvents = true;
		}

		void CodeChanged(object sender, FileSystemEventArgs e)
		{
			string fileName = Path.GetFileNameWithoutExtension(e.FullPath);
			string typeName = controllersNamespace+"."+fileName;
			CompilerParameters options = CreateCompilerOptions();

			CSharpCodeProvider provider = new CSharpCodeProvider();
			CompilerResults compilerResults = provider
				.CompileAssemblyFromFile(options, e.FullPath);

			container.Kernel.RemoveComponent(typeName);
			
			if(compilerResults.Errors.HasErrors)
				return;

			Type type = compilerResults.CompiledAssembly.GetType(typeName);
			container.AddComponent(type.FullName, type);
		}

		private CompilerParameters CreateCompilerOptions()
		{
			CompilerParameters options = new CompilerParameters();
			options.GenerateInMemory = true;
			options.GenerateExecutable = false;
			options.ReferencedAssemblies.Add(assembly.Location);
			foreach (AssemblyName name in assembly.GetReferencedAssemblies())
			{
				Assembly loaded = Assembly.Load(name.FullName);
				options.ReferencedAssemblies.Add(loaded.Location);
			}
			options.IncludeDebugInformation = true;
			return options;
		}
	}
}

And in the global.asax:

public class GlobalApplication : UnitOfWorkApplication
{
	public override void Init()
	{
		HotSwap swap = new HotSwap(directory, 
			Container, 
			Assembly.GetExecutingAssembly(), 
			"MyApp.Controllers");
		swap.Start();
	}
}