What it Does?
To see please download the Demo.zip extract and run.You will see a form with button , click button chose the color and click ok. The form color will change to cool gradient. Actually its a gradient panel control which is created by GradientPanelFactory plugin present in PlugIns folder. The plugin in is loaded dynamically at runtime. The plugin in turn relies on Owf.Controls.A1Panel.dll which contains this cool gradient panel control. But Owf.Controls.A1Panel.dll is not present as separate file in this application not even in PlugIns folder. Then how it works? This missing dll is actually compressed in to zip file and added as embedded resource to plugin dll. And this missing dll is extracted by 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 its done.
Introduction: Recently our project ran in to 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 server along with all dependency dlls. And client application downloads these assemblies when it requires the functionality. Need not to say the client application is loading assemblies dynamically via reflection.
As per current implementation we do not have much control over where the dependency dlls gets downloaded. Also it is became tedious to figure out their path and ensure that 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 app such dlls were responsible for validation of cell phones manufactured in factory which produces 4 lack cell phones per day. Imagine that the dll loaded is wrong and it is not able to detect any fault in product. By the time the product goes in to market and customer comes back with complaint millions of faulty phones will be manufactured.
The solution that I found is to combine everything in to single package i.e. the main dll and its dependency dlls in to 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 wrong dll is added as 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 in to 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 the part of 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 in to 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 think about possible options for working with zip. The 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 project without depending on any third party lib.
Everything sounds good as an idea, but while implementing I ran in to one issue. When we load any assembly, the runtime do 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 that 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 wither the calling method is inclined or not by JIT. Inspecting all assemblies at runtime is also 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 as assembly name as key.
For the purpose of 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 dells 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 purpose of each one. 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 method to
LoadAssembly() which takes file path and loads file using
<code>(). 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 same assembly in the zip resource it is added only once from the assembly where it 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 dll file having 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.
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 application. The method should return a Panel.
Third project is GradientPanelFactory. Assume this as a plug-in developed by customer to customize the application. The class will inherit from
AbstractPanelFactory. The client is interested to have gradient panel so they 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 zip in to single file. Adding multiple zip files is also possible. I have set the output path of this project to plug-in folder of WinForm applocation.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.
Fourth project is a Winform application. The application relies on Loader class in 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 calls the
GetPanel method and docks this panel in form. Even thought the 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 FormLoad and ResolveHandler executes when first time GetPanel button is pressed.
In server client application this approach can simplifies upload and download and resolving assemblies and risk of loading wrong dll is also reduced. If there is 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 drawaback I can see as of now is the little extra work on developers part to create the zip file and add it as embedded resource in project.
I got the idea from here.
I goggled further and found more info.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 working example. Let me know if any
one found this post relevant , interesting or useful.