Picture the scenario. You have a .NET windows service running on a 1000 servers and one of the assemblies needs an update. How does each service pick up the update?
There are two options
- Package and deploy your application to each of the 1000 servers as you normally do.
- Have your service look at a network share, determine if a new assembly is present, copy it over and start using it.
Every company has different procedures for handling code updates. If the second option is a viable option for you, then read on and find out how to do it.
How does an Assembly get loaded
Every .NET app starts with a single Application Domain. You can think of an App Domain as a windows process providing isolation between processes on the same machine.
When a .NET application starts, many assemblies are loaded in this app domain. What determines which assemblies are loaded is based on the runtime and the assemblies you reference in your project.
By using the method System.AppDomain.CurrentDomain.GetAssemblies() we can get a list of the loaded assemblies. I have a demo application called Client.WinForms.exe which is launched and immediately the assemblies loaded are listed.
The default app domain name is the same name as the executing assembly. If we are to use different versions of a referenced assembly then we need to be able to unload and load them when the application is running.
How to Unload an Assembly
So first off, you cannot unload an assembly. That’s right, once it is loaded in an app domain it stays there until the app domain is unloaded.
In order to unload an assembly we need to create a second app domain and load the assembly in that app domain and when we finish executing it, unload the app domain.
At first glance you may be thinking fine let’s use a second app domain, but hold on. We cannot add a reference to the assembly we want to unload otherwise it will be loaded in our default app domain. So we need to structure our application in a way that prevents this. The following steps shows how we achieve this.
Structuring our Application
This demo actually uses two different assemblies with different versions to make the demo easier. In reality you would have one assembly that just has the version updated with each new release. 188.8.131.52, 184.108.40.206, etc.
Looking at the diagram below we see four assemblies. The Client.WinForms.exe is the application and it needs to dynamically load either Class.Library.v1.dll or Class.Library.v1.dll however; we cannot add a reference to these assemblies. Instead an assembly called Class.Marchall.dll is created to act as a marshal between the dll’s and exe. The green and pink arrows shows the project references.
Our simple WinForms app has three buttons. One shows the assemblies loaded and the other two call a method in v1 and v2 of our assemblies. The example below shows the output from pressing Execute Version 1.
We can see at the bottom that the second app domain My Demo Domain has Class.Library.v1.dll loaded and the text right at the bottom is the output from the FetchData() method confirming the assembly name and version being used.
The following code shows what the two button Execute Version 1 and Execute Version 2 do.
Line 62 and 67 calls our InvokeMethod() and passes the name of the assembly and the class within that assembly which contains the method we will call.
Line 74 and 75 creates our second app domain with the friendly name My Demo Domain.
Line 79 creates an instance of the class using the assembly and type passed in. The IMarshall interface is stored in Class.Marshall.dll. It gets the assembly from the BaseDirectory of the current domain. At build time there is a post build task that copies the assemblies there.
Line 81 calls the method FetchData() which executes in the Class.Library.v1.dll or Class.Library.v2.dll, depending on which button is clicked.
Line 90 unloads the app domain which in turn is unloaded the assemblies.
There is only a single interface defined in this assembly.
The code is again very simple. The class implements our IMarshall and inherits from MarshalByRefObject. MarshalByRefObject enables access to our objects across application domain boundaries. Refer to line 79 in the code above.
In a real world scenario we would have
- An assembly called MyAssembly.dll version 220.127.116.11 on our local machine.
- Our app will run and the first thing it will do is look at a network share and check the version of MyAssembly.dll there.
- If it is 18.104.22.168 for example, we would copy the file to our local machine
- The app will load it in our app domain and execute whatever we need it to do.
- Once it finishes it will unload the app domain and then repeat.
Word of caution.
When you check the assembly version on the file share, you need to do it in such a way that it does not get loaded into your default app domain. Use the method GetVersionInfo from the System.Diagnostics.FileVersionInfo class.
Source code is here.
Hope this helps.