views:

278

answers:

4

OK, so I have a .NET project that uses plugins. The plugins are implemented as Class Library (DLL) projects that each build in their own folders. The main project doesn't require the plugin DLLs to run, but if they are available, they are used for various optional functionality. Classes from the plugins are loaded by Type.GetType().

However, for my own purposes while testing the software, I'd like to be able to develop the plugins and the main application at the same time. I've created a "master" solution file that references all the projects, so I can set breakpoints and step across assembly boundaries. However, the problem is that the plugins build into their own directories, and so I have to somehow get the plugin DLL files somewhere where Type.GetType() can find them.

Edit: to clarify, my code already enumerates DLL files in the same directory as the .exe and finds classes that match a given interface (using Assembly.LoadFile(), Assembly.GetExportedTypes(), and Type.IsAssignableFrom()). Then the user selects plugins to enable, and I save those choices to the database using Type.AssemblyQualifiedName. Finally, when I need to use functionality provided by the plugin, I use Type.GetType() to load the plugin class. All of this works exactly as intended. It's just that when I build the app, I get a copy of the .exe without any plugins. I'm looking for a way to structure the project and solution files so that I can keep the main application and the plugin projects separate, while still being able to debug them together in Visual Studio. Does that make sense?

So far, these are the ideas I have:

  • Add references to the plugin projects from the main project.
    This has the advantage that it will always work as intended in my debug environment, but will this cause problems when my app is deployed without its plugins? I checked using Dependency Walker and there doesn't seem to be any direct DLL dependency created by the project reference. Are there any hidden issues to know about here?

  • Change all the plugin projects to build into the same target directory.
    This would seem to work well enough, but it also muddies up my build tree, and it would make it harder to develop only one project without having other projects checked out (right now they are all sibling folders in Subversion).

  • Add a post-build script to copy plugins to another directory.
    This is actually what I'm doing at the moment, and it works well enough, but it seems very hackish and fragile. I'd like to switch to another approach.

  • Find a way for Type.GetType() to search other directories for DLLs
    This is similar to how I'd use LD_LIBRARY_PATH on a Unix system. Obviously, I would only want to enable this for Debug builds, since in a Release mode this could cause lots of subtle problems on the user's system. But is this even possible? If so, how?

Interestingly, in this tutorial on the subject, it says:

The first thing to do is reference the class library we just created, and set the build output to the same directory.

That seems suboptimal to me. Is there a better way?

+1  A: 

For a plugin model, I would have a look at Activator.CreateInstance and Activator.CreateInstanceFrom methods or even take a look at Assembly.LoadFrom. These methods allow you to load types from assemblies in directories other than the "traditional" output directories (bin, debug, release).

Forcing Type.GetType to locate your "plugin" assemblies is really dependent on how you modify policy for the loader to search for the assemblies. For example, you can set the AppDomain.PrivateBinPath property to alter the sub directories the loader will look for referenced assemblies.

Concerning the first point "Adding references to plugin projects". Well, this would land one in a difficult situation preventing extensibility in the future. What if I want to produce a new plugin? Whoops, now you don't have a reference to it and so I guess my plugin won't work then? That's why there would need to be another approach. One we we would "probe" for plugin assemblies.

Note
In a plugin model, all plugins should work against a common interface (or abstract class) and it's really that implementation your are looking for in any new plugins you load. For instance:

private void LoadAllPlugins(string path)
{
    foreach(string file in Directory.GetFiles(path, "*.dll"))
    {
        Assembly a = Assembly.LoadFrom(file);
        foreach(Type t in a.GetTypes())
        {
            if (t.GetInterface("IMyPluginInterface", true))
            {
               // we now have a potential plugin type that implements required functionality
            }
        }
    }
}

Lastly, .NET 3.5 has support for a plugin model (System.AddIn) and also check out the Managed Extensibility Framework release from Microsoft that allows developers to build simple composable plugins (http://www.codeplex.com/MEF)

Mike J
Thanks for the response. In fact, the code I have for loading plugins is very comparable to your code example. (I'm using `typeof(IMyInterface).IsAssignableFrom(t)` instead of `t.GetInterface("IMyInterface")`, but the idea is similar.) My question isn't how to load the plugin assemblies, but how to structure my project and solution files to make working with them easier (in particular, debugging across calls back and forth between plugin and app code). Does that make sense?
Daniel Pryden
Ah. It does make sense, I can offer what I've attempted with a plugin model: simply build each plugin separately (either with a script or some post build event) and set their output to the same output as the main application (or an agreed upon output directory). When debugging from the main project, Visual Studio has enough intelligence to find the debug information for each plugin whenever you use a type
Mike J
The debug information files (.PDB) should at least be accessible from the main project, otherwise debugging the plugins won't work. In most cases, if the debug information does not come through, one can try copying them to the main application's output directory.
Mike J
+1  A: 

Simply include the plugin projects within your solution but with only a dependency from the plugins to the 'application' projects. Enforce this by keeping a duplicate solution but with only the application project(s) and ensuring that builds cleanly. You may also want to consider a post build step that attempts a build of each plugin project individually without the others.

One other check is if you want to enforce backward compatibility, or at least know when an obvious break has occurred. This is easiest if you have a test project which references the plug ins by dll reference not project reference (a minor annoyance in a test project). Then by modifying your references path in the project settings you can point to old versions of the plugins. Doing this properly as part o scripted tests is also reasonably simple.

I would second the suggestion of using the MEF system uness your plugins are effectively known at compile time just no which specific version of the dll you plan on loading.

ShuggyCoUk
+1  A: 

I made a plugin system some time ago using reflection. The mechanism is similar to the one used up in drupal that I find quite flexible.

I have two articles explaining it: Sistema de plugins con C#. Parte I. Conceptos and Sistema de plugins con C#. Parte II. El código explicado but I haven't have time to translate them yet (so they're in Spanish). The code nonetheless is commented in English (if I recall correctly).

I will try to explain my approach here.

Basically you have plugins as they're usually defined, classes that implement a simple interface. This interface describes methods for loading, unloading, installing, uninstalling and configuring the plugin.

Then you have two kind of aspects for the plugins.

First a plugin may offer a service to other classes by implementing a public (known) interface and marking itself as a service by using a "ServiceAttribute". The plugin system will then register the service and make it publicly available to anyone interested in using it. For example:

ICrypt crypto = Plugins.Service["Crypt"] as ICrypt;
if (crypto != null)
  crypto.Crypt(data, key, etc)

On the other hand you have Hooks. Hooks are plugin events. You have two attributes HookAttribute and HookableAttribute.

When you declare a method as Hookable you are saying that you expect it to be intercepted by other plugins. For example

[Hookable]
public event TextChanged OnTextChanged;
//.....
if (OnTextChanged != null)
  OnTextChanged(ref myText);

and in other plugin

[Hooks("OnTextChanged")]
public bool MyTextChanged(ref myText)
{
  myText = "Hello from the plugin";
  return true;
}

The hook signature (the delegate) can be unknown as long as the signature match. Again the plugin system handles the matches and brings it up toguether.

It also includes some attributes to express dependencies between plugins and automatic detection and loading and that kind of things. The project itself is LGPL, so you might find some ideas in it or even better... contribute :P

By the way, I use a Plugins subfolder where I place the plugins. I have no problem debugging them from VS as long as the plugin project is part of the solution I'm working on. I don't need to reference them or anyother thing, If the plugin is part of the solution then I'm able to debug it.

Jorge Córdoba
Daniel Pryden
+1  A: 

Setup your projects so that the Executable is compiled to 1 location, and your "plugins" get compiled to the same location. You could use Junctions for making "bin" the path to your final location.

Also in your Plugin project set your executable as the debugging host.

If you haven't done so already, make sure that your interface isn't getting recompiled with each compilation of your Exe (if it's strong named), because this would force you to have to recompile each of your plugins each time you recompile your host exe.

Paul Farry
Yeah, this is pretty much my second option above. Not optimal because it means moving around my source tree, but it does seem like a workable option. Also, +1 for pointing out that the plugins have a dependency on the version of the interface as well.
Daniel Pryden