Thursday, January 10, 2008

Plug-ins and cast exceptions

A common error people post about in the .NET newsgroups is an InvalidCastException being thrown when loading types dynamically, typically for a plug-in mechanism. This article explains the most common reason, and how to avoid it.

The situation
Generally, there are three main types involved in this kind of set-up: the "driver" class (which loads the plug-in assembly), the interface (which plug-ins must implement), and the plug-in class itself. In order to simplify the example, I'm ignoring the normal code which looks for appropriate types within an assembly, and indeed appropriate assemblies within a directory etc. Here is the source code for the three types involved:

Driver.cs:

using System;
using System.Reflection;

public class Driver
{
static void Main()
{
Assembly assembly = Assembly.LoadFrom ("myplugin.dll");
Type t = assembly.GetType ("SamplePlugin");
IPlugin plugin = (IPlugin) Activator.CreateInstance(t);
plugin.SayHello();
}
}



IPlugin.cs:

public interface IPlugin
{
void SayHello();
}



SamplePlugin.cs:

using System;

public class SamplePlugin : IPlugin
{
public void SayHello()
{
Console.WriteLine ("Hello from the sample plug-in.");
}
}



How not to compile the code
With those files all in the same directory, you can compile them with:

csc Driver.cs IPlugin.cs
csc /target:library /out:myplugin.dll IPlugin.cs SamplePlugin.cs



That produces two assemblies: Driver.exe and myplugin.dll. This corresponds to having two different projects in Visual Studio .NET, and both projects containing IPlugin.cs. Unfortunately, when you run it, you get:

Unhandled Exception: System.InvalidCastException: Specified cast is not valid.
at Driver.Main()



What's going wrong?
The cast in the driver code is saying, "Check that the reference actually points to an instance of IPlugin (or it's null), and throw an exception if it doesn't." However, the IPlugin type that it is trying to check against is the one that Driver knows about - the one within the Driver.exe assembly. Unfortunately, the actual type of the object in question is SamplePlugin which implements the IPlugin type which is in myplugin.dll. Even though the names are the same and the contents are the same, the runtime treats them as completely different types. A type is uniquely identified by its fully qualified name and the assembly it comes from. The runtime keeps track of which assembly files it has loaded, but if there were two identical assemblies in two separate files, those would be loaded as two different assemblies - so the types within them would be completely separate.

How can we fix it?
Clearly we need to make sure that the type used for casting is the same one which the object itself is aware of. Basically, we want to make sure that there's only one IPlugin type in memory. There are two simple ways of doing this, although one of them won't work in Visual Studio .NET:

Solution 1: Keep the interface with the driver assembly, and refer to it
Change the compilation process to:

csc Driver.cs IPlugin.cs
csc /target:library /out:myplugin.dll /r:Driver.exe SamplePlugin.cs



Here we have the same assemblies as before, but this time instead of IPlugin being in the plug-in assembly as well as the driver, it's only in the driver, and we tell the compiler to reference the driver assembly when compiling the plug-in. Running it, we get the desired output:

Hello from the sample plug-in.



Unfortunately, this won't work from Visual Studio .NET 2003 - at least, not with the "Add Reference" dialog. Even though the runtime lets an assembly reference an executable assembly, VS.NET 2003 doesn't - if you try to add a reference to Driver.exe, you get an error message. Fortunately, this restriction has been lifted in VS 2005, making this solution or more appealing one. A "newbie trying to help" pointed out to me that you can, however, edit the project file in VS.NET 2003 to add the reference manually. Here's an example:

Name = "IPlugin"
AssemblyName = "PlugInTest"
HintPath = "..\PlugInTest\bin\debug\plugintest.exe"
/>



Solution 2: Separate the interface into its own assembly
Change the compilation process to:

csc /target:library /out:interface.dll IPlugin.cs
csc /r:interface.dll Driver.cs
csc /target:library /out:myplugin.dll /r:interface.dll SamplePlugin.cs



This is equivalent to there now being three projects in Visual Studio .NET - one for each type. The driver and sample plug-in projects need to reference the project providing the plug-in interface. Three assemblies are now generated, interface.dll, Driver.exe and myplugin.dll. Again, the interface only exists in a single assembly in memory, so both the driver casting code and the plug-in class itself will use the same type. The output is as with solution 1 - no exceptions are thrown.

Conclusion
When writing plug-in architectures, make absolutely sure that each type is only present in a single assembly. Apart from some extreme cases (for instance where the same file is loaded multiple times as different assemblies) this will be enough to get rid of any InvalidCastException you may be experiencing. If you still have problems, please either mail me at skeet@pobox.com, or post in the newsgroup. Given that these situations are frequently complicated, it helps to have some short and complete example code to demonstrate the problem.

No comments: