Goals and issues to solve:
- application that is frequently updated, used by customers where each customer may have different version of application
- plugins of the application are deployed to customer and can’t be changed anymore afterwards
- interface of the plugins has to be changed from time to time according needs of new plugins
- plugins are versioned, newer versions of the same plugin usually use newer plugin interface
- plugins may reference dll’s that are used also in the main application
- plugin referenced dll’s may be of older version than those in latest version of application
- multiple plugins may be used from different threads at the same time, multiple versions of the same plugin may be used at same time
- a lot of data / objects are passed to the plugin code and returned back from the plugin
- .NET C# application; no strong-named assemblies
Possible approaches and their limitations
1) Loading all plugins into main appdomain (into application space):
- not a solution as application can’t load multiple versions of the same dll. Loading of second version of the dll will reuse the first loaded version of dll.
- Loading of dependency dll’s will load first versions of the dependencies (and may fail on loading because of that)
2) Loading plugins into separated app-domains without any special handling:
- different versions of plugins are loaded correctly, but their dependencies are usually not, because they are searched in wrong folder. Dependencies of plugins may be loaded correctly, but do not have to.
- Loading of wrong version of dependency will result in error on loading of the plugin
3) Approach 2 with special handling of loading dependencies:
- loads different versions of plugins into separted AppDomains, their dependencies are also loaded only to specified AppDomain. This is great, as it allows to have access to different versions of assembly at the same time
- you can specify path that is used to look for dependencies of the assembly
- issue is that in that separated assembly it is required to load also the class that is taking care of the assembly loading (and its dependencies). This class is required as it serves as a „proxy“ for passing calls from main AppDomain to the child AppDomain
- used AppDomain.CreateInstanceAndUnwrap
4) Approach 3 with special dependency resolver – specifying locations of assembly dependencies
- I was not able to find a way how this can be archieved on our project with no strong-named assemblies.
- There is only a handler that is called if dependant assembly (or specified version) can’t be found, but this is not usually called as most of our assemblies are not strong-named, and in such case any version of assembly is loaded.
Solution for our project is approach 3.
We have to take care about few details:
- each plugin DLL and its dependencies DLL are in separate folder to assure correct loading
- application’s DLL’s that are referenced from plugin DLL’s are compatible – namespaces and interface implementations remain the same; in situation where we have changed namespaces we had to provide wrapper classes existing in original namespaces to server to plugin DLL’s
- Proxy class had to implement AppDomain.CurrentDomain.AssemblyResolve for cases when newer version of dependency DLL is found in the applications folder. This dependency load would fail and this resolver will take over and load correct DLL from plugin’s folder
- all data that are sent across the domains (from application to plugin and back) have to be serializable or deriverd from System.MarshalByRefObject. Objects that are not serializable can’t be transported.
- AppDomain objects lifetime is about 7 minutes by default, after that they get destroyed. But it can be set to ‚forever‘ by changing our code to set lease time to infinite.
- we had implemented cache for the plugins to avoid loading them more often
- AssemblyDomainProxy serves as base class for proxy classes for different types of plugins. It is instantiated from AssemblyCacheSeparateDomains which creates its instance with CreateInstanceAndUnwrap.
- Proxy class gets a desired assembly name as parameter. It loads the desired assembly its dependencies.
- Proxy class serves as a bridge – its calls can be called from main AppDomain and proxy can use the loadedAssembly and its methods.
Sample code : AssemblyCacheSeparateDomains.cs, AssemblyDomainProxy.cs : AppDomainProxy