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>