Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Allowing C# plugins register on application hooks

I am building a .NET based application, and would like to allow a more extensible and pluggable design.

For the sake of simplicity, the application exposes a set of operations and events:

  • DoSomething()
  • DoOtherThing()
  • OnError
  • OnSuccess

I would like to offer "plugins" to be loaded and hook into some of these operations (something like: When Event1 fires, run plugin1).

For example -- run plugin1.HandleError() when the OnError event fires.

This can be done easily with event subscription:

this.OnError += new Plugin1().HandleError();

The problem is that:

  1. My app doesn't know of the type "Plugin1" (it is a plugin, my app does not reference it directly).
  2. Doing so will instantiate the plugin before time, something i do not want to do.

In "traditional" plugin models, the application ("client" of plugins) loads and executes the plugin code at certain key points.For example -- an image processing application, when a certain operation is performed).

The control of when to instantiate the plugin code and when to execute it are known to the client application.

In my application, the plugin itself is to decide when it should execute ("Plugin should register on the OnError event").

Keeping the plugin "execution" code alongside "registration" code poses an issue that the plugin DLL with will get loaded into memory at registration time, something i wish to prevent.

For example, if i add a Register() method in the plugin DLL, the plugin DLL will have to get loaded into memory in order for the Register method to be called.

What could be a good design solution for this particular issue?

  • Lazily loading (or offering lazy/eager loading) of plugin DLLs.
  • Allowing plugins to control which various parts of the system/app they hook into.
like image 941
lysergic-acid Avatar asked Apr 24 '12 22:04

lysergic-acid


3 Answers

You are trying to solve a non-existing problem. The mental image of all the types getting loaded when your code calls Assembly.LoadFrom() is wrong. The .NET framework takes full advantage of Windows being a demand-paged virtual memory operating system. And then some.

The ball gets rolling when you call LoadFrom(). The makes the CLR create a memory mapped file, a core operating system abstraction. It updates a bit of internal state to keep track of an assembly now being resident in the AppDomain, it is very minor. The MMF sets the up memory mapping, creating virtual memory pages that map the content of the file. Just a small descriptor in the processor's TLB. Nothing actually gets read from the assembly file.

Next you'll use reflection to try to discover a type that implements an interface. That causes the CLR to read some of the assembly metadata from the assembly. At this point, page faults cause the processor to map the content of some of the pages that cover the metadata section of the assembly into RAM. A handful of kilobytes, possibly more if the assembly contains a lot of types.

Next, the just-in-time compiler springs into action to generate code for the constructor. That causes the processor to fault the page that contains the constructor IL into RAM.

Etcetera. Core idea is that assembly content always gets read lazily, only when needed. This mechanism is not different for plugins, they work just like the regular assemblies in your solution. With the only difference that the order is slightly different. You load the assembly first, then immediately call the constructor. As opposed to calling the constructor of a type in your code and the CLR then immediately loading the assembly. It takes just as long.

like image 123
Hans Passant Avatar answered Nov 15 '22 07:11

Hans Passant


What you need to do is to find the path of the dll, and then create an assembly object from it. From there, you will need to get classes you wish to retrieve (for instance, anything that implements your interface):

var assembly = Assembly.Load(AssemblyName.GetAssemblyName(fileFullName));
foreach (Type t in assembly.GetTypes())
{
  if (!typeof(IMyPluginInterface).IsAssignableFrom(t)) continue;
  var instance = Activator.CreateInstance(t) as IMyPluginInterface;
  //Voila, you have lazy loaded the plugin dll and hooked the plugin class to your code
}

Of course, from here you are free to do whatever you wish, use methods, subscribe to events etc.

like image 42
Oskar Kjellin Avatar answered Nov 15 '22 06:11

Oskar Kjellin


For loading plugin assemblies I tend to lean on my IoC container to load the assemblies from a directory (I use StructureMap), although you can load them manually as per @Oskar's answer.

If you wanted to support loading plugins whilst your application is running, StructureMap can be "reconfigured", thus picking up any new plugins.

For your application hooks you can dispatch events to an event bus. The example below uses StructureMap to find all registered event handlers, but you could use plain old reflection or another IoC container:

public interface IEvent { }
public interface IHandle<TEvent> where TEvent : IEvent {
    void Handle(TEvent e);
}

public static class EventBus {
    public static void RaiseEvent(TEvent e) where TEvent : IEvent {
        foreach (var handler in ObjectFactory.GetAllInstances<IHandle<TEvent>>())
            handler.Handle(e);
    }
}

You can then raise an event like so:

public class Foo {  
    public Foo() {
        EventBus.RaiseEvent(new FooCreatedEvent { Created = DateTime.UtcNow });
    }
}

public class FooCreatedEvent : IEvent {
    public DateTime Created {get;set;}
}

And handle it (in your plugin for example) like so:

public class FooCreatedEventHandler : IHandle<FooCreatedEvent> {
    public void Handle(FooCreatedEvent e) {
        Logger.Log("Foo created on " + e.Created.ToString());
    }
}

I would definitely recommend this post by Shannon Deminick which covers many of the issues with developing pluggable applications. It's what we used as a base for our own "plugin manager".

Personally I would avoid loading the assemblies on demand. IMO it is better to have a slightly longer start up time (even less of an issue on a web application) than users of the running application having to wait for the necessary plugins to be loaded.

like image 32
Ben Foster Avatar answered Nov 15 '22 06:11

Ben Foster