Tuesday, May 15, 2007
There is always some part of a system that works, but is far from elegant.  It annoys, but it is never critical enough or broken "enough" to warrant replacing it.  Like a car radio with a stripped knob -- it's painful to change the volume, but not enough to warrant a new car stereo.

The system that consumes the majority of my cycles is primarily a web application. But as its goal is to crunch lots of numbers it contains a "batch" feature. This lets a user kickoff some data-intensive processing and get an email notification when the analysis is complete instead of having to maintain an active browser session.

Behind the scenes the analysis is performed by a service that uses identical copies of the web app's library assemblies to perform the analysis.  Works like a charm.  The only problems came at deployment time.  The service needed to have its assemblies and assorted configuration files updated in tandem with the web app. Nothing a script can't handle, but still another source for confusion and error when something wasn't synched.  There was also something kludgey about having to maintain two copies of everything that bugged me. It irked me every time I had to rev the application.

Yes, I could have shoved the assemblies into the GAC, but I still would have had an issue with the configuration files, not to mention the hassle of having to use strongly named assemblies and no longer being able to do FTP deployment. So, I lived with the kludge and went on to more pressing issues (aka adding features). 

Eventually a more interesting issue arose.  I installed demo version of the app on our production server to display a feature that wasn't ready to go live.  But I couldn't use the "batch" feature for the demo app.  The service (converted from its really annoying incarnation as a WinForms app) was tied to a single web application installation via a database, in this case the production database.  I could have easily configured the service to work against multiple databases, but that would have only worked if the installations shared the exact same code and configuration. This was not the current case and likely would never be.  I also didn't want to create a separate service for each app.  Not scalable, dull, dull, dull.

What I needed was a service that could support N installations of the app.

It seemed straightforward enough.  An earlier refactoring had transformed the bulk of the configuration files into tables in the database, leaving only the assemblies and a single configuration file to deal with.  I knew assemblies could be loaded dynamically from files, so I thought it would be fairly trivial.  I'd just create an Interface to allow the service to hook into the newly loaded assembly, do its thing and then unload it.

Two problems to solve.  Getting the proper assemblies loaded, and getting them reading the proper configuration file.  I thought the latter would be the harder one to solve and it was initially. Once I worked around that, I thought I was home free, but then I ran into the brickwall. I discovered that there's no Assembly.Unload method

If I couldn't unload the first app's assembly, I couldn't load the second's, third's, etc.  So, dynamic loading was out.  I briefly toyed with going to a Remoting solution, but that would have entailed creating a shadow web service for each app, which would have created more configuration and deployment foo.  I knew dynamic loading was what I needed. I felt it could be done -- this is exactly what the ASPNET process does when it loads a new application -- but I could find nothing that would let me do it.

More searching and I chanced upon this sample, and discovered exactly what I needed!

"Additionally, this sample shows how to achieve AppDomain isolation for loading of assemblies to maintain security boundaries for untrusted assemblies."

An AppDomain is one of the classes that no one notices, but is used in every application.  It's the container that holds an application and keeps it safe and separate from other applications.  It's one of the pieces of .NET that makes it much harder to bring a server to it knees when an app misbehaves.

99.9% of the time that's all the AppDomain is asked to do.  But, fortunately, it is capable of so much more.

It would give me an isolated place to load the necessary assemblies.  But how to tell it what configuration file to use?  WinForm and service apps have their configuration files located in the /bin directory, while the assemblies I wanted to use would be plucked from web apps. These have their configuration files sitting in the bin directory's parent.  A little doc time led to the AppDomainSetup class which is held by an AppDomain's SetupInformation property.  It tells the runtime exactly how the AppDomain should be configured.  Perfect!

So I added this method to the class representing an installation of our web app...
Private Function GetCoupler() As IBatchCoupler
        Dim CouplerProxy As IBatchCoupler = Nothing
ThisLogger.DebugFormat("Creating Coupler for {0}", mName)

            Dim DomainSetupInfo As AppDomainSetup = New AppDomainSetup()
            DomainSetupInfo.ConfigurationFile = Path.Combine(mFilePath, "web.config")
            DomainSetupInfo.ApplicationBase = Path.Combine(mFilePath, "bin")
            DomainSetupInfo.ShadowCopyFiles = "true"
Dim domain As AppDomain = AppDomain.CreateDomain(mName.Replace(" ", "") & "AppDomain", AppDomain.CurrentDomain.Evidence, DomainSetupInfo)

            'Create remote object in new appDomain via the coupler interface
            'to avoid loading the design library into the calling application's primary appDomain
            CouplerProxy = domain.CreateInstanceFromAndUnwrap(Path.Combine(DomainSetupInfo.ApplicationBase, "Design.dll"), "BigApplication.Design.BatchCoupler")

ThisLogger.DebugFormat("Coupler created for {0}", mName)

        Catch ex As Exception

        End Try
        Return CouplerProxy
    End Function

Voila, I had an interface to an object in a dynamically loaded app domain.  And since each installation had it's own AppDomain, I didn't need to worry about the loading and unloading the domains.  They were all separated. I could create the coupler once when the Installation class was loaded and forget about it.  Sweet!

The key statements in the method are...

The AppDomainSetup properties: ConfigurationFile, ApplicationBase, and ShadowCopyFiles.  The first is self explanatory.  The ApplicationBase tells the AppDomain where the assemblies are, and ShadowCopyFiles tells the AppDomain to make shadow copies of the files and load from those so that the original files aren't locked.  This is how the ASPNET process loads assemblies.

The setup information is passed to the CreateDomain method, which creates our new AppDomain.  The Evidence parameter is used to determine to figure out what security context the code with be run in.  Need to read some more about how exactly this works.  In this case I'm just passing the evidence from the service's AppDomain.

Finally, CreateInstanceFromAndUnwrap creates our object from the supplied assembly file and returns a reference to it,  ready to use.  Simply calling CreateInstanceFrom would return a handle to the created Object. The AndUnwrap at the end of the method name signifies that the method unwraps the returned handle to supply the actual object. 

Compiled it, deployed it, and...it blew up (according to the log file).   Someone, ahem, had forgotten that the coupler object needed to inherit from MarshalbyRefObject to cross the AppDomain boundary.  Fixed that, and then there was an event argument I had to mark as Serializable, so it too could cross the boundary.  I tried it again, and by gum, the darn thing worked.  A little tweaking and tightening and I had a service that could be configured to run multiple applications without having to keep multiple copies of assemblies and configuration files synched. I cannot explain how great it felt to simplify deployment and expand the service's functionality with the new implementation.

I stood up and planted my left foot upon my chair and struck a commanding, forward looking pose. While my journey did not take it's intended route, I had (for the time being) triumphed. I had conquered a foe and in the process became the master of my AppDomain.

Tuesday, May 15, 2007 10:53:31 PM (Eastern Standard Time, UTC-05:00)   #     Comments [0]  | 

Theme design by Dean Fiala

Pick a theme: