4

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).

namespace Plugin1
{
    public class MyPlugin : IPlugin
    {
        public MyPlugin()
        {
        }
    }
}

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();
        }
    }
}
/// <summary>
/// If using this class.  You can comment/uncomment at the lines provided
/// where Assembly.Load* vs. Assembly.ReflectionOnlyLoad* are used.
/// You should notice Assembly.Load* succeeds in determining type equality;
/// however, Assembly.ReflectionOnlyLoad* fails.
/// </summary>
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)
                {
                    LoadAssembly(dll.FullName);
                }
            }

            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= CurrentDomain_ReflectionOnlyAssemblyResolve;
        }

        private void LoadAssembly(string path)
        {
            //From within the PluginInspectionDomain, load the assembly in a reflection only context.
            Assembly[] loadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
            //TODO (toggle comment): 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)
            {
                //Assembly already loaded.  No need to search twice for plugins.
                return;
            }

            //Assembly not already loaded, check to see if it has any plugins.
            Assembly assembly = Assembly.ReflectionOnlyLoadFrom(path);
            //TODO (toggle comment): Assembly assembly = Assembly.LoadFrom(path);
            GetPlugins(assembly);
        }

        private Assembly CurrentDomain_ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
        {
            //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);
            //TODO (toggle comment): 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)
        {
            using (Stream resource = assembly.GetManifestResourceStream(assembly.GetName().Name + ".Plugin.config"))
            {
                if (resource != null)
                {
                    //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("Configuration contains no plugins.");
                    }

                    foreach (Plugin pluginDescriptor in configuration.Plugins)
                    {
                        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."));
                    }
                }
            }
            return null;
        }
    }
}

If curious about how I used XML to specify plugin configurations check out these files. Please note that the XML parsing is not necessary for reproducing the issue. You could, for example, hard code some strings of the types to search and compare against, but this is how I chose to do it. This also provides a reference for those interested in replicating my plugin loader.

XML Schema: PluginConfiguration.xsd

    <?xml version="1.0" encoding="utf-8"?>
<xs:schema id="PluginConfiguration"
    targetNamespace="clr-namespace:PluginAPI;assembly=PluginAPI/PluginConfiguration.xsd"
    elementFormDefault="qualified"
    xmlns="clr-namespace:PluginAPI;assembly=PluginAPI/PluginConfiguration.xsd"
    xmlns:mstns="clr-namespace:PluginAPI;assembly=PluginAPI/PluginConfiguration.xsd"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="PluginConfiguration">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="Plugin" type="PluginType" minOccurs="1" maxOccurs="unbounded"/>
      </xs:sequence>
      <xs:attribute name="Author" type="xs:string"/>
      <xs:attribute name="Version" type="xs:string"/>
    </xs:complexType>
  </xs:element>

  <xs:complexType name="PluginType">
    <xs:attribute name="Name" type="xs:string" use="required"/>
    <xs:attribute name="MainClass" type="ClassType" use="required"/>
  </xs:complexType>

  <xs:simpleType name="ClassType">
    <xs:restriction base="xs:string">
      <xs:annotation>
        <xs:documentation>
          Matches class names that follow the format:  IDENTIFIER(.IDENTIFIER)*
          This pattern is necessary for validating fully qualified class paths.
        </xs:documentation>
      </xs:annotation>
      <xs:pattern value="^([_a-zA-Z]+([_a-zA-Z0-9])*)(\.?[_a-zA-Z]+([_a-zA-Z0-9])*)*$"/>
    </xs:restriction>
  </xs:simpleType>
</xs:schema>

PluginConfiguration.cs

namespace PluginAPI
{

    /// <remarks/>
    [XmlType(AnonymousType = true, Namespace = "clr-namespace:PluginAPI;assembly=PluginAPI/PluginConfiguration.xsd")]
    [XmlRoot(ElementName = "PluginConfiguration", Namespace = "clr-namespace:PluginAPI;assembly=PluginAPI/PluginConfiguration.xsd", IsNullable = false)]
    public partial class PluginConfiguration
    {

        private Plugin[] pluginField;
        private string authorField;
        private string versionField;

        /// <remarks/>
        [XmlElement("Plugin", typeof(Plugin))]
        public Plugin[] Plugins
        {
            get { return this.pluginField; }
            set { this.pluginField = value; }
        }

        /// <remarks/>
        [XmlAttribute()]
        public string Author
        {
            get { return this.authorField; }
            set { this.authorField = value; }
        }

        /// <remarks/>
        [XmlAttribute()]
        public string Version
        {
            get { return this.versionField; }
            set { this.versionField = value; }
        }
    }

    /// <remarks/>
    [XmlType(AnonymousType = true, Namespace = "clr-namespace:PluginAPI;assembly=PluginAPI/PluginConfiguration.xsd")]
    public partial class Plugin
    {
        private string mainClassField;
        private string nameField;

        /// <remarks/>
        [XmlAttribute()]
        public string MainClass
        {
            get { return this.mainClassField; }
            set { this.mainClassField = value; }
        }

        /// <remarks/>
        [XmlAttribute()]
        public string Name
        {
            get { return this.nameField; }
            set { this.nameField = value; }
        }
    }
}

Plugin.config (which is an embedded resource in the plugin assembly).

<?xml version="1.0" encoding="utf-8" ?>
<PluginConfiguration xmlns="clr-namespace:PluginAPI;assembly=PluginAPI/PluginConfiguration.xsd"
                     Author="Nicholas Miller"
                     Version="1.0.0">
  <Plugin MainClass="Plugin1.MyPlugin" Name="MyPlugin"/>
</PluginConfiguration>
Community
  • 1
  • 1
Nicholas Miller
  • 4,205
  • 2
  • 39
  • 62
  • 4
    There seems to be a lot of irrelevant code in there. Is this a [Minimal, Complete, and Verifiable example?](http://stackoverflow.com/help/mcve) – Matias Cicero Oct 27 '16 at 19:28
  • 1
    why not just use if (o is IPlugin) { }? – SledgeHammer Oct 27 '16 at 19:41
  • @MatiasCicero, but you understand the code and the explanation, right?....It seems a very complete and verifiable example to me. – Hackerman Oct 27 '16 at 19:44
  • @Hackerman You missed the *minimal* part – Matias Cicero Oct 27 '16 at 19:48
  • 1
    The code is completely supplementary, useful for replication. The minimal part is everything before the first horizontal line. If you feel that some code should be highlighted in this section, please let me know. – Nicholas Miller Oct 27 '16 at 19:50
  • @SledgeHammer Nope, can't do it. These are types, not objects. If I use objects, that defeats the purpose of using a reflection-only context. – Nicholas Miller Oct 27 '16 at 19:53
  • @MatiasCicero, it seems like a complex question to me, also with the minimal code included to understand and reproduce the problem :) – Hackerman Oct 27 '16 at 19:53
  • @NickMiller The point is, if you remove what is below that line, the code you have provided becomes no longer verifiable (reproducible). There must be a way to reproduce what you're stating without having to create a needless configuration class or XML parser, which ends up being code noise, irrelevant to the original problem. – Matias Cicero Oct 27 '16 at 19:53
  • @MatiasCicero Yes, you are correct in that the XML is not *necessary*. It would be far simpler to hard code the type names. My intent was to publish it anyways since it is in the spirit of plugin loading, not sure if that is acceptable or not. – Nicholas Miller Oct 27 '16 at 20:04

0 Answers0