jeudi 27 octobre 2016

Why are identical types not equal?

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