Loading and Unloading Assemblies in AppDomains with
Shadow Copying
The sample code for this post
can be found here.
When designing a distributed application infrastructure, I
like to build as much “plug-n-play” into the design. By creating
and implementing interfaces into many of components and leveraging
System.Runtime.Reflections, I can build a system that dynamically loads
assemblies to process the incoming requests from the presentation layer. By using
interfaces, if I want to change the functionality in the mid-tier, I
don’t have to worry about changing anything in the presentation layer as
long as I implement the interface in the business component that the
presentation layer is expecting. One problem that still remains with this
approach of dynamically loading assemblies and calling the contained types
through the interfaces is being able to change the implementation on the fly
without having to restart any middleware components. Once you load an assembly
into a domain, it remains there until you unload that domain or you restart the
process. If you load assemblies into your default domain, that means you have
to restart the process you are executing in. Until that process is started,
you can not copy or update the loaded assembly on disk until you restart. It
would be nice to dynamically load and execute assemblies and update those
assemblies in your deployment directory whenever you needed a newer version to
take effect the next time it is loaded.
Enter “AppDomains” and their
“ShadowCopyFiles” property. By creating new AppDomains and setting
particular properties on the type, you now gain the ability to dynamically load
and unload assemblies from memory along with being able to physically update
the assembly on disk even if it’s loaded into memory. This now adds a
tremendous amount of flexibility with deployment efforts and provides you with an
infrastructure to keep your applications up and running while making changes.
In the following text, I’ll show you how to enable your applications to
take advantage of these techniques. Mind you, you need to perform some
up-front planning and design work with your application to allow for
dynamically loading assemblies even if you don’t create your own
AppDomains. This post further assumes you already have an
application/component that dynamically loads assemblies and executes methods on
those types through interfaces (if not, I’ll provide some sample code to so you how do
that).
The first thing we want to do is setup the interface the
calling component will utilize to call the method implementation provided in
the dynamically loaded assemblies. In this sample “plug-n-play”
architecture, the calling component does not care about what’s in the
pluggable components just as long as they implement the interface it knows
about. Here’s what the sample interface looks like.
public interface IProcess
{
bool Execute(String xml);
}
All components that will plug-in to the application
architecture will implement this interface. There will be code in the calling
component to ensure all assemblies loaded implement it.
Now we have to provide to the calling component some
configuration information that defines the available plug-in components it can
call. For this sample, we simply use the App.config file to store the key and
the assembly name and type we want to dynamically load. Here’s what the
sample uses to get the corresponding assembly type and name for a particular
key.
<appSettings>
<add key="Assem1" value="AssemToLoad.Class1,
AssemToLoad" />
</appSettings>
In this particular example, the key we will use is
hard-coded into the program. We could have just as easily provided a list box
or other UI component to allow a key to be selected from a collection. In middleware,
we could have used an XPATH expression into an XML document being passed around
to extract some information to be used as our key. It all depends on the
application, but you get the point.
As mentioned earlier, all plug-in components must implement
the interface because this is the “contract” the calling component
will use to execute the implementation code. The sample simply creates a class
that implements the interface. It’s pretty generic and does nothing more
than echo the passed in string with a wrapper to the debugging console.
Here’s the class.
public class Class1 : MarshalByRefObject,
AssemInterface.IProcess
{
public Class1()
{
}
#region
IProcess Members
public bool Execute(string xml)
{
Debug.WriteLine("----> Execute: "+xml+" <----");
return true;
}
#endregion
}
You’ll notice this class implements the interface we
defined earlier. It guarantees us the calling program/component will be able
to load this assembly and call the Execute method which is a member of the
IProcess interface. Notice also this class is derived from the
MarshalByRefObject class. Most of the time you see this class used when
deriving objects that will be remoted from one process to another. It will
become more apparent later why we need to derive from this class.
Now that we have our interface, or “contract”
that both the caller and callee know about and understand, we can move onto the
calling component. The implementation of this component is a simple WinForm
application as it provides an easy way to show and test how this pluggable
architecture works. When requested to load and execute the assembly, this code
will:
1.) Create a new AppDomain
with ShadowCopyFile turned on
2.) Lookup the assembly to
load from a configuration file
3.) Load it into the new
AppDomain
4.) Call the
implementation method through the interface
5.) Unload the assembly by
unloading the AppDomain
Here’s the code that creates the AppDomain.
AppDomainSetup
setup = new AppDomainSetup();
setup.ShadowCopyFiles
= "true";
setup.ShadowCopyDirectories
=
Path.GetDirectoryName(Application.ExecutablePath);
setup.ApplicationName
= "TheAppDomain";
AppDomain
domain = AppDomain.CreateDomain("myAppDomain",
null, setup);
The first thing we do is create a new instance of the
AppDomainSetup object. With this object, we can configure the creation
parameters of the new AppDomain we will create. The ShadowCopyFiles property
is set to “true”. Notice this is a string and not a bool (not sure
why they did it that way :->). Next, we set the ShadowCopyDirectories
property to the directory the calling component is executing from. In this
sample, all pluggable components will also reside in this directory. We did
this to make sure that all assemblies loaded from this directory would be part
of the Shadow Copy. The string can have multiple directories all separated by
a semi-colon. In this case, when an assembly is loaded into the domain we are
creating, a copy will be made in another directory and loaded from there. This
directory is the shadow copy directory. There are other properties that you
can change to define where the files will be shadow copied to, but I left that
off as an exercise for the reader.
To finish up, we assign an application name to this setup
object and then call the static method AppDomain.CreateDomain to create the
domain we will start loading assemblies into. We pass the AppDomainSetup
object we created so the AppDomain is created with the settings we want, in
particular, the ShadowCopyFiles settings.
Now that are AppDomain is created, we need to find our
assembly and load it into the new AppDomain. Here’s the code that does
that. It simply takes information from the App.Config file.
String
assemKey = ConfigurationSettings.AppSettings["Assem1"];
string[] s =
assemKey.Split(new char[]
{ ','} );
String
assemType = s[0].Trim();
String
assemName = s[1].Trim();
This reads the assembly information from the configuration
file and splits out the name and type to be loaded. Now, we just need to load
the assembly into our new domains. One line of code does that.
object obj =
domain.CreateInstanceAndUnwrap(assemName, assemType);
The assembly has been loaded into our new domain and is
ready to be called. You would think at this point we have a reference to an
object running another AppDomain. However, since AppDomains isolate and
logically partition assemblies, we can’t call objects across AppDomains
boundaries. So what is this “object”. It’s actually a proxy
similar to a client proxy that’s generated when you’re working with
remoted objects. Thus the reason for having our class that’s loaded
dynamically being derived from the MarshalByRefObject class. This allows the
class to be remoted across AppDomain boundaries.
We’ll now use the interface we created earlier to call
the implementation method. Before blindly calling the method on the interface,
we’ll check it to ensure the type implements the interface we’re
going to use. If it doesn’t, we could throw an exception at this point
or gracefully display a message to the user.
AssemInterface.IProcess iAppReqProc = null;
if (obj is
AssemInterface.IProcess)
{
iAppReqProc = obj as
AssemInterface.IProcess;
bool retVal =
iAppReqProc.Execute("<root></root>");
}
The last bit of cleanup we now want to perform after calling
the method is to unload the assembly from memory. Although you can’t do
this directly, you can do it by unloading the AppDomain you’ve created
earlier. Unloading the AppDomain will also unload all the assemblies that are in
the AppDomain. The sample has this line of code in a finally block to ensure
it gets called no matter what happens. It’s just a one line call to a
static method, like so.
AppDomain.Unload(domain);
Now, because we’ve created a new AppDomain, we can
load and unload assemblies dynamically. But more important, we can also update
those images on disk while they are loaded and executing as well. This is
because the files, prior to being loaded, were copied to another directory and
loaded from there. It makes for a real nice deployment scenario. Just XCOPY
the files to your deployment directory and the next time it gets loaded, the
new version is used.
If you want to know more about AppDomains and how to set
them up, I highly recommend the MSDN library and to read through the AppDomain
class along with the AppDomainSetup class. Here’s some additional links
to other blogs that delve further into more detail than what I’ve
provided here.
Good luck with your pluggable architectures!
http://msdn.microsoft.com/asp.net/community/authors/royosherove/default.aspx?pull=/library/en-us/dncscol/html/csharp05162002.asp
http://blogs.msdn.com/junfeng/archive/2004/02/09/69919.aspx
http://blogs.msdn.com/jasonz/archive/2004/05/31/145105.aspx