Best Practices for Assembly Loading for portable applications
Some of you have certainly met with many different aspects of developing applications based on extensions. The following publication raises the question of practices for extensions in portable applications.
On msdn pages you will find well designed in theory and with almost identical title as ours: Best Practices for Assembly Loading . In short - we get a knowledge of the 'pros and cons' of different approaches and although unfortunately article lacks of practical examples it is worth to get familiar with it.
Reflections on Default Context Loading
GAC (Global Assembly Cache) as a place for our extensions in the case of architecture design for portable solutions is certainly not appropriate. The contents of the GAC may vary drastically between different operating systems and as a result - does not guarantee the consistency.
A good solution to prevent overfilling base directory is to put extensions in subdirectories. You can very easily define additional search paths (relative to the base directory) for extensions in the configuration file.
<?xml version="1.0"?> <configuration> <runtime> <assemblyBinding xmlns="urn: schemas-microsoft-com:asm.v1"> <!-- you can set more than one probing path --> <probing privateBinPath="Plugins;Plugins2\SubPlugins"/> </assemblyBinding> </runtime> </configuration>
Application domain search path can also be easily defined in the code by creating a new, additional AppDomain.
static class Program { static void Main() { AppDomainSetup pluginsDomainSetup = new AppDomainSetup { ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, PrivateBinPath = @" Plugins1;Plugins2\SubPlugins" }; AppDomain pluginsDomain = AppDomain.CreateDomain (Guid.NewGuid().ToString(), AppDomain.CurrentDomain.Evidence, pluginsDomainSetup); IPlugin plugin = (IPlugin)pluginsDomain.CreateInstanceAndUnwrap //this call uses full assembly name which is safe ("Plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Plugin.Test"); //actual use of plugin plugin.Use(); AppDomain.Unload(pluginsDomain); } }
NotInheritable Class Program Private Sub New() End Sub Private Shared Sub Main() Dim pluginsDomainSetup As New AppDomainSetup() With { _ Key .ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, _ Key .PrivateBinPath = " Plugins1;Plugins2\SubPlugins" _ } Dim pluginsDomain As AppDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), AppDomain.CurrentDomain.Evidence, pluginsDomainSetup) 'this call uses full assembly name which is safe Dim plugin As IPlugin = DirectCast(pluginsDomain.CreateInstanceAndUnwrap("Plugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "Plugin.Test"), IPlugin) 'actual use of plugin plugin.Use() AppDomain.Unload(pluginsDomain) End Sub End Class
Karmian way
As we read in article indicated above:
For assemblies that are loaded without context, the problem can be caused by using the Assembly.LoadFile method to load the same assembly from different paths. The runtime considers two assemblies that are loaded from different paths to be different assemblies, even if their identities are the same.
In addition to type identity problems, multiple versions of an assembly can cause a MissingMethodException if a type that is loaded from one version of the assembly is passed to code that expects that type from a different version. For example, the code might expect a method that was added to the later version.
More subtle errors can occur if the behavior of the type changed between versions. For example, a method might throw an unexpected exception or return an unexpected value.
We do not use multiple application domains and Assembly.Load method as we encountered problems during objects serialization. What we use is Assembly.FileLoad because this way we avoid LoadFromContext exception that is thrown when no valid AppDomain is configured for assembly file path and Assembly.Load method is used. When we load those assemblies we put them in IDictionary
As you can see this pattern is very flexible - code can be easily modified to load extensions from multiple paths (including network sources), dynamic assemblies, streams and other.
using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Windows.Forms; namespace Karmian.Core.Providers { [Serializable] internal class CorePluginProvider { public CorePluginProvider() { Initialize(); } public IDictionary<string, Assembly> Plugins { get; private set; } public void Initialize() { Plugins = new SortedDictionary<string, Assembly>(); foreach (var file in Directory.GetFiles( AppDomain.CurrentDomain.BaseDirectory + @"Plugins", Application.ProductName + ".Plugin.*.dll", SearchOption.TopDirectoryOnly)) Plugins.Add(AssemblyName.GetAssemblyName(file).FullName, Assembly.LoadFile(file)); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; } private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { if (!Plugins.ContainsKey(args.Name)) throw new InvalidOperationException( String.Format("Could not determine valid assembly in context for assembly name '{0}'.", args.Name)); return Plugins[args.Name]; } } }
Imports System.Collections.Generic Imports System.IO Imports System.Reflection Imports System.Windows.Forms Namespace Karmian.Core.Providers <Serializable> _ Friend Class CorePluginProvider Public Sub New() Initialize() End Sub Public Property Plugins() As IDictionary(Of String, Assembly) Get Return m_Plugins End Get Private Set m_Plugins = Value End Set End Property Private m_Plugins As IDictionary(Of String, Assembly) Public Sub Initialize() Plugins = New SortedDictionary(Of String, Assembly)() For Each file As var In Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory & "Plugins", Application.ProductName & ".Plugin.*.dll", SearchOption.TopDirectoryOnly) Plugins.Add(AssemblyName.GetAssemblyName(file).FullName, Assembly.LoadFile(file)) Next AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf CurrentDomain_AssemblyResolve End Sub Private Function CurrentDomain_AssemblyResolve(sender As Object, args As ResolveEventArgs) As Assembly If Not Plugins.ContainsKey(args.Name) Then Throw New InvalidOperationException([String].Format("Could not determine valid assembly in context for assembly name '{0}'.", args.Name)) End If Return Plugins(args.Name) End Function End Class End Namespace
Comments
Post new comment