In many applications, the ability to dynamically load and execute third-party code (in the form of plug-ins) is highly desirable. Plug-ins can be used to provide:

  • Alternative implementations of built-in features
  • Completely new features (given some kind of framework on which they can be built)
  • All functionality in the application (e.g. in a test harness, compiler or other modular system)

There are a number of mechanisms within the .NET Framework to facilitate plug-in code:

Reflection enables assemblies to be dynamically loaded (removing compile-time references), and to iterate through the types in an assembly. To build a plug-in system based entirely on reflection, however, would be limiting, unreliable and very slow. The overheads involved in calling methods via reflection are high. Also, in the absence of a compile-time reference to an object, you lack the ability to verify whether the object contains the method/property you’re trying to access.

Interfaces are one of the key primitives in object orientated programming. They allow you to define the public methods, events and properties of a type without specifying how it should be implemented; or indeed, how it should behave. You can toss around a reference to an object using only its interface type, and still be able to do everything with the object that you could do with a concrete class (save for instantiating it, of course).

An obvious way to implement a plug-in system, therefore, would be to define a set of interfaces that were common to both the application and its plug-ins, implement them in the plug-ins and load them into the application using reflection. The advantage of this approach is that you have a contract at compile-time against which you can guarantee that the methods/properties you’re accessing exist on the object you’ve loaded.

There is quite a significant downside to this approach, however: You are loading third-party, potentially malicious code directly into your application’s memory space. The plug-in code could use reflection to access and manipulate everything in your application, not to mention crash it. It’s not hard to see why this would be a bad idea.

Application domains are an important (though perhaps not widely-understood) part of the .NET Framework. The vast majority of applications will only ever use a single AppDomain but, when utilised, they can be very powerful. Application domains sit at a high level in the runtime, providing a memory space into which the code you reference and execute is loaded. The only way to access managed objects from outside their AppDomain is to serialise them (a process over which you can exercise a lot of control) or to marshal them via remoting. An AppDomain can be secured to prevent another AppDomain from seeing inside it, loading assemblies and reflecting types. It can also raise and handle its own exceptions, keeping it isolated from the main process. This is definitely a solid foundation for a plug-in system.

It makes a lot of sense to load any plug-in code into a separate AppDomain, then ferry objects between the two domains in a controlled, sandboxed fashion. A general rule to consider when writing plug-in code is:

  • Use binary serialisation (the Serializable attribute or the ISerializable interface) when passing data between two application domains. Only ever pass an instance of a type that is common to both domains; e.g. a standard .NET type or a type defined in an assembly that is referenced by both domains. Any operations performed on serialisable types will run in the AppDomain that calls them (i.e. the main application).
  • Use remoting (handled transparently by MarshalByRefObject) when calling methods or raising events. Operations performed on remotable types will run in the AppDomain in which they are instantiated (i.e. the plug-in domain).

Why Not WCF?

At this point, you might well ask, “Why not use WCF as the basis for a plug-in system?”. It’s true, some developers do advocate this practice, but I personally do not. WCF imposes a number of show-stopping limitations on plug-in code:

  • WCF handles events poorly, requiring callback interfaces to be manually defined and wired up. Remoting handles events transparently.
  • Operations on WCF services can only exchange serialisable data. You can’t, for example, pass a remotable object to a WCF service to enable two-way communication.
  • WCF is primarily designed to be stateless. Plug-in code is almost always stateful. Although WCF handles sessions and concurrency, these can be difficult to use.
  • WCF uses XML serialisation based on public members of a type. Remoting uses binary serialisation and has the necessary permissions to access private members of a type.
  • WCF is optimised for interprocess and network-based communication, not communication between two application domains within the same process.

And, of course, it’s worthwhile to note that WCF itself is built on top of .NET Remoting; it is under no threat of deprecation, as it is a fundamental part of the framework.

More About MarshalByRefObject and Remoting

MarshalByRefObject is essential to any non-trivial cross-AppDomain functionality. It is handled specially by the .NET Framework; all you have to do is inherit from MarshalByRefObject and the framework will generate a transparent proxy for your class, automatically marshalling calls between the application domains for you.

Some important things to remember about writing classes that extend MarshalByRefObject:

  • Any objects you pass-to or return-from a MarshalByRefObject must be serialisable, or themselves a MarshalByRefObject.
  • You must remember to mark any types you derive from Exception or EventArgs with the [Serializable] attribute.
  • IEnumerable sequences created using the yield statement or LINQ cannot cross an application domain (because the compiler does not mark them as serialisable). Don’t return sequences from a MarshalByRefObject; instead, copy the elements into a collection (or use the ToList() method) and return the collection.
  • If you pass a delegate to a MarshalByRefObject – and the method it points to is in a different AppDomain – you must ensure that the method belongs to a MarshalByRefObject as well. Do not pass delegates to static methods, because they will be called on the wrong AppDomain (since there is no object instance to marshal the call to).

There are some other common types that can’t cross application domain boundaries:

  • DataObject (used for drag-and-drop, clipboard and other OLE functionality) is neither MarshalByRefObject or serialisable. You can either extract the data and pass it directly to the other AppDomain, or create a wrapper that implements IDataObject and inherits from MarshalByRefObject.
  • Image/Bitmap, although marked as serialisable, may not cross AppDomain boundaries. You should pass a Stream or byte[] containing the image data instead.

Lifetime Services and ISponsor

To further complicate matters, remoting uses lifetime services (rather than ordinary generational garbage collection) to determine when instances of a MarshalByRefObject should be cleaned up. By default, you have a window of 5 minutes in which to use an object obtained from a foreign AppDomain before the proxy becomes disconnected and an exception is thrown upon access. This is a necessary evil, because the garbage collector can’t count references to a remotable object inside a different application domain; that would break the isolation provided by application domains in the first place.

There are two methods to get around this, however:

  • Override the InitializeLifetimeService() method and return a null reference. This instructs remoting not to clean up instances of your object in another AppDomain. This has the potential to create memory leaks, so you can really only use this technique for singleton classes.
  • Obtain the lifetime service object (ILease) from the MarshalByRefObject using the RemotingServices class and register an ISponsor object to keep the instance alive.

Sponsorship works by renewing the lease on a MarshalByRefObject; it does this by returning a TimeSpan indicating how much longer the object is needed. Remoting will periodically call the Renewal() method on an ISponsor object until it returns a timespan of zero, or the sponsor is unregistered.

// register a sponsor
object lifetimeService = RemotingServices.GetLifetimeService(myMarshalByRefObject);
if (lifetimeService is ILease) {
    ILease lease = (ILease)lifetimeService;
    lease.Register(mySponsor);
}

// unregister a sponsor
object lifetimeService = RemotingServices.GetLifetimeService(myMarshalByRefObject);
if (lifetimeService is ILease) {
    ILease lease = (ILease)lifetimeService;
    lease.Unregister(mySponsor);
}

In practice, what this means is that you should hold a reference to a sponsor for any MarshalByRefObject you obtain from another AppDomain for as long as you need to access the object. When the sponsor object becomes eligible for garbage collection, it will also take out the remotable object which it sponsors. Ideally, implementations of ISponsor should be serialisable.

In my implementation of a plug-in system, I created a convenient generic class, Sponsor<TInterface>, which is simultaneously responsible for registering/unregistering a sponsor, accessing the remotable object itself and providing the renewal logic. You hold a reference to the sponsor object in your class, then call its Dispose() method when the remotable object is no longer needed. My plug-in system centers around the Sponsor class; ensuring that objects from the plug-in AppDomain are always wrapped in a Sponsor instance and never returned directly to user code without one.

Design for a Plug-In System

As I have alluded to, a plug-in system based on reflection, interfaces, remoting and sponsors is built around two application domains. The main AppDomain uses the PluginHost class to create the plug-in AppDomain and remotely instantiate PluginLoader, the class that loads plug-ins and instantiates remotable objects:

// create another AppDomain for loading the plug-ins
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = Path.GetDirectoryName(typeof(PluginHost).Assembly.Location);

// plug-ins are isolated on the file system as well as the AppDomain
setup.PrivateBinPath = @"%PATH_TO_BINARIES%\Plugins";

setup.DisallowApplicationBaseProbing = false;
setup.DisallowBindingRedirects = false;

AppDomain domain = AppDomain.CreateDomain("Plugin AppDomain", null, setup);

// instantiate PluginLoader in the other AppDomain
PluginLoader loader = (PluginLoader)domain.CreateInstanceAndUnwrap(
    typeof(PluginLoader).Assembly.FullName,
    typeof(PluginLoader).FullName
);

// since Sandbox was loaded from another AppDomain, we must sponsor
// it for as long as we need it
Sponsor<PluginLoader> sponsor = new Sponsor<PluginLoader>(loader);

PluginLoader dynamically loads the plug-in assemblies (located in a subdirectory) into the plug-in AppDomain:

foreach (string dllFile in Directory.GetFiles(pluginPath, "*.dll")) {
    Assembly asm = Assembly.LoadFile(dllFile);
    Assemblies.Add(asm);
}

PluginLoader keeps a cache of ConstructorInfo objects for each interface implementation it discovers, so it can quickly instantiate objects. It exposes GetImplementations (returns IEnumerable<TInterface>) and GetImplementation (returns the first implementation of TInterface).

private IEnumerable<ConstructorInfo> GetConstructors<TInterface>() {
    if (ConstructorCache.ContainsKey(typeof(TInterface))) {
        return ConstructorCache[typeof(TInterface)];
    }
    else {
        LinkedList<ConstructorInfo> constructors = new LinkedList<ConstructorInfo>();

        foreach (Assembly asm in Assemblies) {
            foreach (Type type in asm.GetTypes()) {
                if (type.IsClass && !type.IsAbstract) {
                    if (type.GetInterfaces().Contains(typeof(TInterface))) {
                        ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
                        constructors.AddLast(constructor);
                    }
                }
            }
        }

        ConstructorCache[typeof(TInterface)] = constructors;
        return constructors;
    }
}

private TInterface CreateInstance<TInterface>(ConstructorInfo constructor) {
    return (TInterface)constructor.Invoke(null);
}

public IEnumerable<TInterface> GetImplementations<TInterface>() {
    LinkedList<TInterface> instances = new LinkedList<TInterface>();

    foreach (ConstructorInfo constructor in GetConstructors<TInterface>()) {
        instances.AddLast(CreateInstance<TInterface>(constructor));
    }

    return instances;
}

PluginHost calls the GetImplementation/GetImplementations methods on PluginLoader to return transparent proxies to the remotable objects instantiated from the plug-ins. It wraps them in a Sponsor instance and returns them to the user. PluginHost also handles reloading/unloading of the AppDomain.

Putting It All Together

The general usage pattern for my plug-in system would be:

  1. Create a series of interfaces and place them in a common assembly.
  2. Create one or more plug-in assemblies containing types that implement the interfaces.
  3. Create an application which references only the common assembly.
  4. Instantiate PluginHost, passing the path to load plug-ins from.
  5. Call the LoadPlugins() method and check for success.
  6. Instantiate implementations of the plug-in interfaces using the GetImplementations() or GetImplementation() methods.
  7. Keep a reference to the Sponsor<TInterface> object returned from the above methods until the object is no longer required.
  8. Unload the plug-in AppDomain by calling Dispose() on the PluginHost object.

You can see an example of this in the included example project.

Final Words

Nobody can deny that loading third-party code in a separate application domain is regarded as best practice. Hopefully, this task is greatly simplified through the use of the plug-in system i’ve provided. It is, of course, simply a proof of concept implementation. Other things you might want to consider would be:

  • Applying security to the plug-in AppDomain to further sandbox the environment.
  • Filtering the plug-in assemblies loaded; either according to digital signatures, implementation of marker interfaces or particular metadata.
  • Making metadata about the plug-ins available to calling code.
  • Handling exceptions more gracefully.

In any event, I hope it demonstrates the basic idea behind cross-AppDomain programming in .NET.

Download

PluginSystem.zip (Visual Studio 2010 solution, zipped)

27 thoughts on “A Plug-In System Using Reflection, AppDomain and ISponsor

  1. vote

    Hi, I’m trying to mirgate your code into a ASP.NET project.

    I downloaded you example solution and add a ASP.NET project in it. I just add necessary reference and copy the code from Main() method and paste them into a controller in the newly created ASP.NET project. It seems that The new project didn’t work well. A FileNotFoundException occured, here is the error message, “Could not load file or assembly ‘Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null’ or one of its dependencies. The system cannot find the file specified.”

    Reply
    • vote

      ASP.NET projects typically run in a more restrictive, sandboxed environment, so the runtime may not have permission to dynamically load assemblies. You could try making the Common project a strong-named assembly and installing it in the Global Assembly Cache (GAC). Alternatively, you may need to set certain options in the web.config file to allow probing for private assemblies and request appropriate permissions to use reflection. I am not hugely familiar with ASP.NET myself, so I cannot point you to any specific resources.

      Reply
  2. +1
    vote

    Excellent article. I was looking for an example that did not use System.Addin. I’ve been testing with that… It is good, but overly complex. I was thinking about how to version the contracts in your design. I presume I could just have an Adapter assembly that adapts from the common interface to whatever a new interface might be. Thanks for this.

    Reply
    • vote

      Thanks 🙂 As far as versioning goes, there’s no perfect solution there (it’s always possible for a developer to change the original interface and leave any existing plug-ins stranded), but I imagined I would take inspiration from COM when my code matured enough to think about new versions. It’s common in that framework for new versions of interfaces to extend the old ones, and to use the naming convention Interface, InterfaceV2, InterfaceV3, etc. With that approach, you always maintain backwards-compatibility and the consumer can query an object to see whether it supports the newest interface or not.

      Reply
    • 0
      vote

      Great article Brad.

      Another approach to the interface versioning issue is to define all the interfaces in a common assembly and add it is a reference with Embed Interop Types set to true.

      If you then decorate your interfaces with ComImport and a Guid, type embedding will match up these interfaces even if they’re different. A newer version of the interface (without breaking changes of course, like changed signatures) will happily load up older versions embedded inside the plugins.

      You can even write a little wrapper class around the interface within the host which traps for MissingMethodException and the like (which will be thrown when an old plugin doesn’t support a new feature) and behave accordingly (e.g. return null).

      Reply
        • vote

          If any of your Interfaces are generic, you will be unable to embed those types, and you’ll get compile errors. There are presumably no tricks to making that work, so sign + GAC is probably the best approach?

          Reply
          • vote

            I’ve placed generic interfaces in the common assembly and did not encounter any problems. I did not have to sign the assembly or place it in the GAC.

          • vote

            Sorry for the confusion.

            My point was that you cannot mark generic Interfaces with ComImport() and Guid() and hope to set ‘Embed Interop Types’ = true on the Common assembly’s reference in a consuming assembly (ie – in a plugin or plugin-calling code).

            FWIW, I was never able to get this sample code to execute without exception when the Plugin’s Common reference has a version mismatch with the calling code’s Common: Always get cannot cast SomeType to SomeType`1 (even if those are interfaces).

            Since our existing plugins need to function even after the Common assembly has been updated, the goal was to load plugins from a folder with all their references. Obviously this version mismatch issue puts the kibosh on that.

            My hopeful solution would’ve been ComImport(), however since those interfaces are generic, that won’t work. I could probably re-structure the plugin framework such that interfaces are not generic, but instead additive (ie: Class : ITypeA, ITypeAChild), but decided to forgo this for now..

            Of course, maybe that’s overthinking it and there would be an easier way to avoid the version mismatch?

  3. +1
    vote

    How this can work ? Your Sponsor is [Serializable].
    See: http://msdn.microsoft.com/en-us/magazine/cc300474.aspx

    “Because the sponsor is called across an AppDomain boundary, the sponsor must be a remotable object, meaning it must be marshaled either by value or by reference. If you derive the sponsor from MarshalByRefObject, the sponsor will reside on the client’s side, and it can base its decision of the renewal time on client-side events or properties that it is monitoring. That raises an interesting question: if the lease keeps the remote server object alive, and if the sponsor keeps the lease alive, who keeps the sponsor alive? The answer is that somebody on the client side must keep a reference on the sponsor, typically as a class member variable. Doing so also allows the client to unregister the sponsor when the client shuts down. The client can also unregister the sponsor in its implementation of IDisposable.Dispose. Unregistering a sponsor improves overall performance because the lease manager will not spend time trying to reach a sponsor that is not available.
    If you only mark the sponsor as serializable, when you register the sponsor it will be marshaled by value to the host’s side, where the sponsor will reside. This will eliminate the marshaling overhead of contacting the sponsor, but it will also disconnect it from the client. A marshaled-by-value sponsor can only base its decision on information available on the host’s side.”

    Reply
    • vote

      When I was first writing my plug-in system, I referred to that article, however I found the reality was contrary to the information presented in the article. When my Sponsor class was originally derived from MarshalByRefObject, I found that the lease manager never invoked the Renewal() method, and the remote objects became disconnected from the client after the usual timeout period (about 5 minutes). When switching to [Serializable], the lease manager called the Renewal() method when expected, and the objects remained alive until I called the Dispose() method on the sponsor (or waited for garbage collection to occur). I can assure you that my technique has been extensively tested and is used in production code.

      Reply
  4. vote

    Great article! Thank you a lot for this really useful information. But is there any way to load an user controll through the app domain boundary? If I’m trying to load a control from an other domain and attach it in the main domain to a panel, I get an exception (like property ‘parent’ can not be accessed).

    Reply
    • vote

      Unfortunately, this is not possible. Windows Forms controls cannot be passed between application domains. It is possible code executing in another appdomain to create and display a separate form, but you cannot combine controls from different domains on the same form.

      Reply
      • +2
        vote

        I’ve actually got it today: I’m creating the control in the other domain and binding it to a panel with system’s SetParent:

        [DllImport("User32.dll")]
        public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndParent);
        SetParent(PluginView.Handle, this.Handle);

        It works fine!

        Reply
  5. vote

    Friend, this example is incredible! Easily the best out there I’ve found in my hours of searching for information on AppDomains. I’m using a modified version of this in my upcoming game and of course will be including both your copy-write notice and a credit to your site in it.

    Reply
  6. Pingback: C# Plug-In Architecture Articles | Reader Man Blog

  7. vote

    Hi there!
    Very interesting article. Thanks for sharing your thoughts!
    I am searching for a up to date solution for a plug-in and out modding solution for my application. Is this approach still after 7 years later your preferred method? Would you change something?
    I would also like to see this on your GitHub account. Users could then add your “final thoughts”.

    Best regards
    Christian

    Reply
    • vote

      I still use essentially the same solution in production code today, although I abandoned my ISponsor implementation in favour of the built-in ClientSponsor class. Depending on how you want your plug-ins to work, it may also be more desirable to use an IoC container instead of directly instantiating classes using reflection.

      While this solution is robust when everything is implemented correctly, you need to have a good working knowledge of how remoting works behind the scenes, and it will impose certain restrictions on how you write your plug-in code. When things go wrong (usually because you forget to sponsor an object from the other AppDomain), it can be difficult to debug. You definitely want to be sure that the benefits you will get from the isolation of your plug-ins are worth the effort required to get it working.

      I’ll consider putting the code on GitHub at some point, but i’d really like to have another look at it first.

      Reply
  8. vote

    Hi !!

    I have a similar infrastructure for plugins. I have Object created in separate appDomain. Object created in separate appDomain is later initialized via an API which takes references to object created in Main appDomain.

    PluginDependent obj1;

    PluginLoad obj;
    obj.Initialize(obj1);

    PluginDependent object will marshalled across appDomain. What i am not sure is, who should sponsor PluginDependent object and how life time of that sponsor will be managed.

    Reply
    • vote

      These days, I use the built-in ClientSponsor class. Whichever AppDomain the object was created in, the sponsor is created in the other domain. You call the Register method when you start using the object, and as soon as it is no longer required, you call either Unregister or Close (former for a single object, latter for multiple objects). This is similar to the IDisposable pattern.

      I should point out that AppDomains are being removed in .NET 5, so it’s probably not a good idea to develop anything new that relies on them.

      Reply
      • vote

        So basically you are stating that as soon Initialize method (in example above), is called, i create ClientSponsor and register PluginDependent object with ClientSponsor.

        For .NET 5, could you please point to link with info about discontinuation of AppDomain concept if you have that handy.

        Thanks for quick response 🙂

        Reply
        • vote

          Another point was that if i provide null Lease object won’t be disconnected from remoting infrastructure. But if we unload the appDomain, object would anyway get destroyed.correct. so in that sense we won’t need to provide any sponsorer

          Reply
          • vote

            Unloading an AppDomain is an expensive operation, so you don’t want to do it very often. If you provide null leases to all remote objects, then you will eventually run out of memory. It is better to keep the domain loaded for as long as possible and only sponsor objects for as long as you need them.

  9. vote

    Basically i will create fix number of remote objects and once user session is closed all appDomains are unloaded. So remote objects are created very often within a particular session. But will consider all points and work on proper stratergy.

    Reply

Leave a reply to Christian Cancel reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required