I was developing a small plugin framework and noticed that I couldn't successfully use Type.IsAssignableFrom() to check if a plugin class implements the type IPlugin. I had checked other SO questions (such as this) to see why the function returned false, but to my surprise, none of the suggestions worked.
I had a class inside a plugin assembly which implements the PluginAPI.IPlugin class in a referenced assembly. When checking the AssemblyQualifiedName for both my PluginAPI.IPlugin type as well as the types for the plugin class's list of interfaces, I found no difference whatsoever. Both outputed the value:
PluginAPI.IPlugin, PluginAPI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Upon further investigation, I checked Type.cs source and found that the function IsAssignableFrom() was failing in an internal call where it checks for type equality (via ==).
I had found that modifying my plugin framework to load plugin assemblies in a typical execution context (Assembly.LoadFrom) as opposed to a reflection only context (Assembly.ReflectionOnlyLoadFrom) allowed the type equality check to evaluate to true, which is what I had expected all along.
Why is it that the reflection-only context causes the types to no longer be equal?
Code:
The following section contains relevant code for reproducing the problem as well as description on the projects were set up.
Assembly 1: PluginAPI
This assembly only contains IPlugin.cs:
namespace PluginAPI
{
    public interface IPlugin
    {
    }
}
Assembly 2: Plugin1
This assembly contains several plugins (classes that implement PluginAPI.IPlugin). There is Calculator.cs and WPFPlugin1.cs
namespace Plugin1
{
    public class Calculator : IPlugin
    {
        public Calculator()
        {
        }
    }
}
namespace Plugin1
{
    public class WPFPlugin1 : IPlugin
    {
        public WPFPlugin1()
        {
        }
    }
}
Assembly 3: AppDomainTest
This assembly contains the entry point and tests loading the plugin assemblies in a separate AppDomain so they can be unloaded.
namespace AppDomainTest
{
    public class AppDomainTest
    {
        public static void Main(String[] args)
        {
            AppDomain pluginInspectionDomain = AppDomain.CreateDomain("PluginInspectionDomain");
            PluginInspector inspector = new PluginInspector();
            pluginInspectionDomain.DoCallBack(inspector.Callback);
            AppDomain.Unload(pluginInspectionDomain);
            Console.ReadKey();
        }
    }
}
namespace AppDomainTest
{
    [Serializable]
    public class PluginInspector
    {
        public void Callback()
        {
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_ReflectionOnlyAssemblyResolve;
            //TODO: Change this to the output directory of the Plugin1.dll.
            string PluginDirectory = @"H:\Projects\SOTest\Plugin1\bin\Debug";
            DirectoryInfo dir = new DirectoryInfo(PluginDirectory);
            if (dir.Exists)
            {
                FileInfo[] dlls = dir.GetFiles("*.dll");
                //Check if the dll has a "Plugin.config" and if it has any plugins.
                foreach (FileInfo dll in dlls)
                {
                    Console.WriteLine(String.Format("DLL Found: {0}", dll.FullName));
                    LoadAssembly(dll.FullName);
                }
            }
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= CurrentDomain_ReflectionOnlyAssemblyResolve;
        }
        private void LoadAssembly(string path)
        {
            Console.WriteLine(String.Format("\tLoading Assembly"));
            //From within the PluginInspectionDomain, load the assembly in a reflection only context.
            Assembly[] loadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
            //Assembly[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            AssemblyName assemblyName = AssemblyName.GetAssemblyName(path);
            bool assemblyAlreadyLoaded = loadedAssemblies.Any(new Func<Assembly, bool>((Assembly a) =>
            {
                //If the assembly full names match, then they are identical.
                return (assemblyName.FullName.Equals(a.FullName));
            }));
            if (assemblyAlreadyLoaded)
            {
                Console.WriteLine("Assembly already loaded: {0}, exiting function early.", assemblyName.FullName);
                return;
            }
            //Assembly not already loaded, check to see if it has any plugins.
            Assembly assembly = Assembly.ReflectionOnlyLoadFrom(path);
            //Assembly assembly = Assembly.LoadFrom(path);
            GetPlugins(assembly);
        }
        private Assembly CurrentDomain_ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
        {
            Console.WriteLine("Resolving assembly: {0}", args.Name);
            //This callback is called each time the current AppDomain attempts to resolve an assembly.
            //Make sure we check for any plugins in the referenced assembly.
            Assembly assembly = Assembly.ReflectionOnlyLoad(args.Name);
            //Assembly assembly = Assembly.Load(args.Name);
            if (assembly == null)
            {
                throw new TypeLoadException("Could not load assembly: " + args.Name);
            }
            GetPlugins(assembly);
            return assembly;
        }
        /// <summary>
        /// This function takes an assembly and extracts the Plugin.config file and parses it
        /// to determine which plugins are included in the assembly and to check if they point to a valid main class.
        /// </summary>
        /// <param name="assembly"></param>
        public List<IPlugin> GetPlugins(Assembly assembly)
        {
            Console.WriteLine("Searching for plugins inside assembly: " + assembly.FullName);
            using (Stream resource = assembly.GetManifestResourceStream(assembly.GetName().Name + ".Plugin.config"))
            {
                if (resource != null)
                {
                    //For brevity I have ommitted the types PluginConfiguration and Plugin from the PluginAPI assembly
                    //since they are only container classes generated from XML that have string properties.
                    //These are used to find which classes should be checked for implementing IPlugin.
                    //Parse the Plugin.config file.
                    XmlSerializer serializer = new XmlSerializer(typeof(PluginConfiguration));
                    PluginConfiguration configuration = (PluginConfiguration)serializer.Deserialize(resource);
                    if (configuration == null)
                    {
                        Console.WriteLine("Configuration is null.");
                    }
                    if (configuration.Plugins == null)
                    {
                        Console.WriteLine("Could not find any plugins.  It's null.");
                    }
                    Console.WriteLine();
                    foreach (Type type in assembly.GetExportedTypes())
                    {
                        Console.WriteLine("Type: FullName=\"{0}\"", type.FullName);
                    }
                    Console.WriteLine("\nAuthor: {0}\tVersion: {1}", configuration.Author, configuration.Version);
                    Console.WriteLine("========================================");
                    foreach (Plugin pluginDescriptor in configuration.Plugins)
                    {
                        Console.WriteLine(String.Format("Plugin: Name={0}, MainClass={1}", pluginDescriptor.Name, pluginDescriptor.MainClass));
                        bool containsType = false;
                        foreach (Type type in assembly.GetExportedTypes())
                        {
                            if (type.FullName.Equals(pluginDescriptor.MainClass))
                            {
                                containsType = true;
                                if (typeof(IPlugin).IsAssignableFrom(type))
                                {
                                    Console.WriteLine("MainClass \'{0}\' implements PluginAPI.IPlugin", pluginDescriptor.MainClass);
                                }
                                Console.WriteLine("Checking for {0}", typeof(IPlugin).AssemblyQualifiedName);
                                Console.WriteLine("Interfaces:");
                                foreach (Type interfaceType in type.GetInterfaces())
                                {
                                    Console.WriteLine("> {0}", interfaceType.AssemblyQualifiedName);
                                    if (interfaceType == typeof(IPlugin))
                                    {
                                        //This is NOT executed if in reflection-only context.
                                        Console.WriteLine("interface is equal to IPlugin");
                                    }
                                }
                            }
                        }
                        Console.WriteLine((containsType ? "Found \'" + pluginDescriptor.MainClass + "\' inside assembly.  Plugin is available." : "The MainClass type could not be resolved.  Plugin unavailable."));
                    }
                }
            }
            Console.WriteLine("Done searching for plugins inside assembly: " + assembly.FullName + "\n");
            return null;
        }
    }
}
Aucun commentaire:
Enregistrer un commentaire