What It Does?
To see, please download the demo, extract and run. You will see a form with button, click button, choose the color and click ok. The form color will change to cool gradient. Actually, it's a gradient panel control which is created by
GradientPanelFactory plugin present in PlugIns folder. The plugin is loaded dynamically at runtime. The plugin in turn relies on Owf.Controls.A1Panel.dll which contains this cool gradient panel control. But you will not see this DLL anywhere with this application, not even in PlugIns folder. Then how does it work? This missing DLL is actually compressed into a zip file and added as embedded resource to plugin DLL. And this missing DLL is extracted by the application at runtime from zip resource of plugin DLL. So the plugin can be self contained and only single file irrespective of the number of dependency DLLs. Read further to see why and how it's done.
Recently, our project ran into one issue which is client server application. We have exposed some extension points for our customer for customization of application. The customers can implements their own logic by implementing specific interfaces or
abstract class or applying attributes to the class. The custom logic assemblies need to be uploaded on the server along with all dependency DLLs. And client application downloads these assemblies when it requires the functionality. I need not say that the client application is loading assemblies dynamically via reflection.
As per the current implementation, we do not have much control over where the dependency DLLs get downloaded. Also it is became tedious to figure out their path and ensure that the correct dependency DLLs will be loaded. Loading a wrong DLL could be disastrous depending upon the importance of logic to be executed. In one of my apps, such DLLs were responsible for validation of cell phones manufactured in factory which produces 4 lac cell phones per day. Imagine that the DLL loaded is wrong and it is not able to detect any fault in the product. By the time the product goes into the market and customer comes back with complaints, millions of faulty phones will be manufactured.
The solution that I found is to combine everything into single package, i.e., the main DLL and its dependency DLLs into one unit. So there will not be any need to upload multiple files and download many files. We can get rid of file path related issues. The possibility of loading wrong DLLs is also very minimal unless the wrong DLL is added as a resource. There will always be one assembly which holds all the required DLLs along with it. Visual Studio allows adding any file as embedded resource into a project. Right click on project in solution explorer, choose Add -> Existing Item -> Select File. Make sure that you select All files option in File Dialog to view DLL files. After adding file, set the Build Action property to Embedded Resource. After compilation, the resource files becomes part of the assembly. All dependency DLLs can be added as embedded resource into the project. When code inside this assembly will be executed, it will require these dependency DLLs. Since runtime will not be able to find these assemblies, we need to handle
AppDomain.Resolve event to load the DLLs in
AppDomain. The handler will retrieve the DLLs from the assembly resource.
To further improve this idea, we can in fact compress all the DLLs into a zip and then add as embedded resource. This can considerably reduce the size of assembly. This makes more sense if number of files are more and size as well specially when transferred over network. However this also means that we need to find this zip from Assembly resource and then extract the required assembly from zip before loading in Resolve Handler. So I started thinking about possible options for working with zip. A couple of very good libraries are available for this purpose. DotNetZip is very easy to use and SharpZipLib is also a popular option. However I decided to use another option ZipStore which is a C# class available at Codeplex. Since it is just a single class file, I can use it directly in the project without depending on any third party lib.
Everything sounds good as an idea, but while implementing I ran into one issue. When we load any assembly, the runtime does not raise
AssemblyResolve event until we try to use the type inside assembly that in turn tries to use the type in dependency assembly. Suppose assembly A depends on B. So B is dependency assembly. When you try to use any type from A that depends on type in B at that time the
AssemblyResolve is raised if runtime is not able to find the assembly B. Now when
AssemblyResolve handler runs, there is no way to figure out which assembly is requesting the required DLL, i.e., A in above example. But we have to extract the required assembly DLL from the resource of requesting assembly (Assembly A). I was hoping that
ResolveEventArgs.RequestingAssembly property will help, but it was always
null. Then I was hopping from
Assembly.GetCallingAssembly() but the result of this varies depending on whether the calling method is inclined or not by JIT. Inspecting all assemblies at runtime is also a very costly option. So as a workaround while loading assembly, I am inspecting if assembly DLL has any zip file as resource. If it has, then I inspect zip for DLL files. Then I am adding the required information to one dictionary with assembly name as key.
For the purpose of the demo, I will use a winform application which will load the plug-in from \\PlugIns folder in its base directory (i.e. location of EXE file). The valid plug-in needs to extend
AbstractPanelFactory class and DLL name should end with PanelFactory.dll. The plug-in needs to implement the
abstract GetPanel method which returns the
Panel based on color argument. Now assume that this plug-in gets downloaded at application startup from server. And there will be only one DLL for plug-in, the dependency DLLs will be extracted at runtime from zip added as embedded resource from the plug-in DLL.
The solution for this demo contains four projects. I will explain the purpose of each one.
The first one is
AssemblyLoader - this project contains a single
static class responsible for loading and resolving the assemblies. It has only two
public members, one is the Boolean property
ResolveAssembly. When set to
true, this class hooks to
AssemblyResolve event. It has a method to
LoadAssembly() which takes file path and loads file using
Assembly.LoadFrom(). While loading, it also inspect assembly resource to find zip files and also extract information about files available in zip. This information is added to one dictionary. The implementation makes sure that if two plug-ins have the same assembly in the zip resource, it is added only once from the assembly where it was found first. Also a cross verification is done using crc32 values in zip metadata that two files in different zip resources are same. There should not be a DLL file having the same name but having totally different assembly or different version of same assembly. To ensure that all assemblies get loaded from expected locations, you can hook
AppDomian.AssemblyLoad event which fires when assembly is loaded. The handler for
AssemblyResolve event will extract the required DLL from zip resource of DLL.
The second project is
AbstractPanelFactory, this has only one
abstract base class with only one
abstract method. This acts as an extension point for customer. By extending this class, they can customize the application. The method should return a
The third project is
GradientPanelFactory. Assume this as a plug-in developed by the customer to customize the application. The class will inherit from
AbstractPanelFactory. The client is interested to have gradient panel so she/he uses a third party library Owf.Controls.A1Panel.dll. Now this library is added as an embedded resource in this project. In case of multiple assemblies, they can be added to folder and sub folders, then zipped into a single file. Adding multiple zip files is also possible. I have set the output path of this project to plug-in folder of WinForm application. However I have set copy local property of Owf.Controls.A1Panel.dll to
false so that it do not get copied to plug-in folder. Remember we are only downloading one plug-in assembly and not its dependencies.
The fourth project is a Winform application. The application relies on
Loader class in the first project to load plug-in DLL and resolve missing assemblies. This application downloads (assume so) and loads plug-in DLL and creates the instance of class inherited from
AbstractPanelFactory. Then it calls the
GetPanel method and docks this panel in form. Even thought the plug-in (
GradientPanelFactory) depends on Owf.Controls.A1Panel.dll which is not present with this application, still the example works as it gets extracted from the embedded zip resource in GradientPanelFactory.dll in
I request you to download the solution and try yourself. Run
DemoClientApp and click
GetPanel Button, it opens the color dialog, select color, click ok. You should see the gradient panel docked in the form. However the interesting code of loading assembly executes at
ResolveHandler executes when first time
GetPanel button is pressed.
In server client application, this approach simplifies upload and download and resolving assemblies and risk of loading wrong DLL is also reduced. If there is a need to update individual assembly file in zip, then individual file can be uploaded to server and this can be compared to the file in zip before loading in
AssemblyResolve handler and if it differs, sever side version can be loaded.
This can simplify the distribution of desktop application as well and is even easier to implement as zip can be added as resource to EXE file and extracted at runtime. There is no need to figure out requesting assembly as EXE assembly holds all files.
Points of Interest
The only drawback I can see as of now is the little extra work on the developers' part to create the zip file and add it as an embedded resource in project.
I got the idea from here. I Goggled further and found more information. One good post on CodeProject is this which is for EXE application. I wrote this article as it seems to be a good idea worth sharing along with a working example. Let me know if anyone found this post relevant, interesting or useful.
- 13th November, 2011: Initial post