Introducing MonoRail.HotSwap
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(); } }
Comments
It seems like this idea could be extended beyond just the controllers, making dependencies dynamically compilable as well as long as they are accessed through the container. I'm really interested in seeing this idea being fleshed out a bit more.
Master why you summoned me? Ok reading your article right now :)
Finished reading, looks interesting but I also think I have some questions regarding the implementation:
I have to use windsor.... you know I like to ignore configuration so much unless its 100% necessary :P I probably have to dig out the piece of code you have suggested regarding auto registration of controller in windsor.
After I just wrote this first sentence I am wondering if it is possible to ignore Windsor for such implementation- maybe its possible to directly manipulate the IControllerTree in the built in monorail to dynamically replace the controller too?
All too often we need to change more than one controller file (I mean, the related model etc.), I think its not that valuable if changing more than one file would still require a full recompilation.
I suddenly come up with another thought- the main problem we have is the appdomain load/unload issue. The root cause of evil is the appdomain resides inside the assembly that we put our controller (and everything) inside. For example, if we put our model and controllers in a separate library and only load that dynamically into the core asp.net app, would that make us able to skip loading appdomain?
Looks like a good solution. This gave me an idea for my project. Thanks
Ayende,
I did some more research and realize there is actually some changes in asp.net 2.0 which makes the reloading even more frequent :(
http://weblogs.asp.net/owscott/archive/2006/02/21/438678.aspx
It seems like any simple touch of the folder would just make the entire appdomain restarts. I don't quite like the way you do it as the change scope issue could be a problem.
Maybe IronPython ASP.NET project would give us some more insight? I think we really need to somehow change the way how asp.net monitor its folder- if possible.
Goodwill, there is a possible solution to that problem here:
http://connect.microsoft.com/VisualStudio/feedback/Workaround.aspx?FeedbackID=240686
That said, I can't remember noticing this for a while - surely given that problem, editing even a view would cause an appdomain restart, and I certainly don't see that happening on my machines.
I did a screencast showing how this will affect the speed of development:
http://colinramsay.co.uk/2007/09/23/hotswap-for-monorail-in-action/
I did a screencast showing how this will affect the speed of development:
http://colinramsay.co.uk/2007/09/23/hotswap-for-monorail-in-action/
Colin,
Great links. I think that feedback workaround is important to know. But I still not feeling very comfortable in this idea yet- I suspect how much times this compilation automation would work well. Think we have to do a bit more testing to conclude.
Colin,
Since you have tried this already- wouldn't it be nice if you could also try using multiple classes, something like what we would do usually, for example adding new model class and adding new controller together?
If this thing is just going to hit 30% I don't feel its that much useful :( Which I suspect this is the case.
Comment preview