Ant
1 note

Updateable Add-ins for Web Applications

Writing an Add-in architecture for your project these days is simple, using the right tools, but .NET suffers from a drawback, in that, you cannot unload an assembly once it is loaded into your AppDomain.

So, does this mean you cannot update an add-in without restarting your web-application?

There is a loop-hole, you can load two copies of the assembly into the same AppDomain.

So here's the plan

  1. Create a contract for a service in an application
  2. Implement that contract a couple of times
  3. Create a web app that knows about the contract
  4. Allow for upload of the implementation to the web app
  5. Store the implementation in a Cache
  6. Copy the implementation with a random name to a Bin (not 'the' bin) directory and load it

Now you can repeat steps 5 and 6 as much as you want.

The drawback of course is that you are loading more and more stuff into the AppDomain, eak! Well actually, I am clearing down the Bin every time the Worker Process recycles - which is going to happen on occasion, and this is not something I'm going to be doing very often.

To the Code

This is an ASP.NET MVC 3 Razor application, and I have created an implementation of IDependencyResolver using Ninject, this is where I have put my code for the module management.

InitModules

This function is called at the start of the application from the HttpApplication.Application_Start() and passes the location for the storage of all this module monkey business.

protected void Application_Start() {
    DependencyResolver.SetResolver(new ApplicationDependencyResolver());

    ApplicationDependencyResolver.InitModules(Server.MapPath("~/App_Data/"));

    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
            
}

private static string _modulesCachePath;
private static string _modulesBinPath;

internal static void InitModules(string storeRootPath) {
    _modulesCachePath = Path.Combine(storeRootPath, "ModulesCache");
    _modulesBinPath = Path.Combine(storeRootPath, "ModulesBin");

    if (Directory.Exists(_modulesBinPath)) {
        // clear down if exists
        try { Directory.Delete(_modulesBinPath, true); } 
        catch (IOException) { } // ignore expected error
    }

    // create storage
    if (!Directory.Exists(_modulesBinPath)) 
        Directory.CreateDirectory(_modulesBinPath);
    if (!Directory.Exists(_modulesCachePath)) 
        Directory.CreateDirectory(_modulesCachePath);

    foreach (var filePath in Directory
                .GetFiles(_modulesCachePath, "*.cachedDll")) {

        LoadModule(filePath);
    }
}
  1. Clears any existing bin, at this point you would expect to be able to as nothing in it is loaded.
  2. Creates the bin and cache directories, if required.
  3. Calls LoadModule for all cached dlls in the cache directory.

LoadModule

This function is called with the path to a cached dll and is responsible for loading up the implementation into the AppDomain and binding the modules for use by the application.

internal static void LoadModule(string cacheImplementationPath) {

    // copy the file to the modules bin, with a random name
    // this allows you to update as many times as you want
    var binImplementationPath = Path.Combine(
        _modulesBinPath, Guid.NewGuid().ToString("N") + ".dll");
    System.IO.File.Copy(cacheImplementationPath, binImplementationPath);

    // load the assembly
    var updatedImplementation = Assembly.LoadFile(binImplementationPath);

    // bind for use
    ((ApplicationDependencyResolver)DependencyResolver.Current).Container
        .Rebind<ISaySomethingModule>()
        .To(updatedImplementation.GetTypes()
            .First(t => typeof(ISaySomethingModule).IsAssignableFrom(t)));

}
  1. Copies the file to the bin, with a random name
  2. Loads the assembly into the AppDomain
  3. Binds the implementation to the contract for use in the application

UpdateModule

This function is called to update the cached implementation with a new one.

internal static void UpdateModule(Stream fileStream, string moduleName) {
    // copy the file to the modules cache
    // only allow for one implementation per moduleName
    var singleImplementationPath = Path.Combine(
        _modulesCachePath, moduleName + ".cachedDll");
    using (var file = System.IO.File
        .Create(singleImplementationPath)) {

        fileStream.CopyTo(file);
    }

    LoadModule(singleImplementationPath);
}
  1. Copies the new assembly to the cache, replacing any old implementation. Because this is never loaded you will have access to do this.
  2. Calls LoadModule to load and bind the new implementation

Usage

I have a controller with two actions;

Index: Shows the result of the call to ISaySomethingModule.Say() using the current implementation, or if none exists, a default

Upload: Accepts a new assembly to update the implementation of ISaySomethingModule

and finally.. The Results

Starting the application you can see the default message as there is no Implementation to call on.

So I browse to, and upload the "Wise" implementation.

This gives me a wise saying...

The Cache has been updated with the new assembly (safely named)

And the Bin contains the loaded version

Now I update again with the other implementation

The cache remains the same as above, but the bin now has a new assembly file which is loaded into the AppDomain

And after recycling the application this is cleared down to contain just the currently loaded implementation.

Happy? Well I am.

Attachments

UpdateableAddin.zip VS2010 ASP.NET MVC Razor


Thanks for the attachement but what FF version do you use? I have a different view a bit.


Post a Note

(required)

(required never shown)

On Twitter Follow MrAntix on Twitter

One hour ago
TheNextWeb
Bing's search API now live on the Windows Azure Marketplace http://t.co/utX8uOuG by @alex

15/05/2012
WindowsAzure
Announcing the MEET Windows Azure Event! Streamed online June 7th. Register at http://t.co/bObzTAuL  #MEETAzure #WindowsAzure

40 minutes ago
commadelimited
Buy the @amazon Kindle version of mine and @cfjedimaster's @jquerymobile book for $10 today: http://t.co/PWRZ2dkd

just now
achirinos
RT @CsharpCorner: "Hello World" Windows Phone Application Using PhoneGap or Cordova: In this article, we will… http://t.co/xAqgMDvN

just now
CsharpCorner
Autoscaling Application Blocks WIndows Azure: Autoscaling Application Blocks can… http://t.co/ryDpLWRE