Search code examples
c#dllplugins.net-assemblyappdomain

Plugin system that keeps alive objects


I have an application that needs to load and unload at runtime some plugins in the form of .dll files. Those dll files contain one or more classes that derive from my abstract class Module which looks like this:

public abstract class Module : MarshalByRefObject
{
    //various fields here, not reported as they are not useful to understand

   public abstract void Print();

}

From what I understood: the only way in C# to unload an assembly and its classes is to put the assembly in a dedicated Appdomain and then unload it when it's not needed anymore and the only way to communicate between AppDomains is to derive from MarshalByRefObject or MarshalByValueObject. Then When I pass a reference of the Module to another AppDomain I don't really get the object but a transparent proxy. Can anyone confirm what i wrote above please?

Ok now the real problem: let's say in my file PersonPlugin.dll I have a "Person" class that extends Module. this class contains the fields name, lastname and phone number.

The method Print, which is called periodically (it doesn't make sense to call print periodically, but it's an example) prints those 3 fields.

Later I build a new dll with a new version of the class Person, that has a new field called address and Print method now prints also the address. NB: Plugins are made by 3rd parties and I can't know if the new version is similar to the older or completely different, but in my case it's safe to assume that a class Person wouldn't differ that much between versions.

I then replace the old dll file with this new file (the dll is shadowcopied in a dedicated appdomain so the file is writable / deletable)

A FileSystemWatcher notices the change and:

  1. creates a new appdomain for the new dll

  2. saves the state of the old instantiated Person objects

  3. assignes the Person objects from the old Person class to the new one

  4. unloads the appdomain containing the old Person class

  5. the application starts printing the new string containing also the address (that is null at least in the beginning)

Points 2 and 3 are the core of my problem: How can I keep alive those objects (or at least their fields) after unloading their class and then instantiate a new class which has the same name and similar properties, but that is in fact another class?

This is what i though so far:

  1. using reflection I can copy in a dictionary every field of every Person object, delete the old objects and then instantiate new ones and copy back the fields where possible (if an old field is not present it's skipped an if a new one is present it gets its default value)

  2. using something like MessagePack to automatically serialize the old Person objects and then deserialize them to new Person objects

However I didn't made any test to see if those 2 solutions may work, mainly because I'd like to know before starting to write code if those 2 solutions may work in reality or just in my mind and because maybe some of you have a more polished / working solution or even better a framework / library which already does that.

UPDATE:

Ok, I realized that asking this question without contextualizing it a bit may lead to some misunderstanding, so: The question I'm asking is about my thesis, which is a modular minimal game engine (not a server). The idea is to make the engine modular and, to make it simple to test various features, its modules can be changed at runtime applying immediately the changes without restarting or loosing the state of the "actors" in the game. A dll contains 1 to many modules. every dll is loaded in 1 AppDomain, so if I unload that appdomain, I unload every module in it. Modules inside the same appdomain can have direct references to eachothers, since when unloaded they are unloaded all together. Modules in different assemblies (so also different AppDomains) communicates using a message bus and never have direct references. If a module consuming the message is unloaded, the system will simply stop forwarding that message to that module.

What is important is that what a module represents is up to the user of the engine: a module may represent a single game object like a car, or a whole physics module. I won't dig into further details as they are useless right now, but what I want to achieve may be exemplified by this:

I have a dll containing a module called Car which always moves forward. I change the dll with a new one containing the module Car that now moves always backward. The result is that once I replace the dll, the car will immediately invert its direction.

Of course this example is stupid, there are methods to achieve this in a far more easy way. However this can also apply in a situation where I find a bug in my code, I correct it, submit it and the bug simply disappears, or even more complicated situations.

That's why I need to keep alive objects (I mean, keep their state alive) and the whole system too.

And about your point 2: there are no hard limits about why I would not allow to coexist 2 modules of the same type but different versions as long as they are in different namespaces or in different dlls since in my system every module is identified and referenced with a different ID


Solution

  • First, let me comment on your first point.

    From what I understood: the only way in C# to unload an assembly and its classes is to put the assembly in a dedicated Appdomain and then unload it when it's not needed anymore and the only way to communicate between AppDomains is to derive from MarshalByRefObject or MarshalByValueObject. Then When I pass a reference of the Module to another AppDomain I don't really get the object but a transparent proxy. Can anyone confirm what i wrote above please?

    Confirmed.

    This is entirely accurate. .NET assemblies cannot be unloaded from an AppDomain after they have been loaded. The entire AppDomain must be unloaded.

    In the case of a modular system you must load the plugins in a separate AppDomain and marshal data between the host app and the plugin.

    Now, to address to your question about loading modules on the fly and persisting their state. You can absolutely do this but you need to establish a contract between your module and your host application.

    There is no need to mark your module as MarshalByRefObject since that will introduce additional restrictions on the module implementation and also brings additional overhead. Instead, I would have my module contract be as light as possible by using an interface to represent the contract.

    public interface IModule
    {
        Person GetPerson();
        void Print();
    }
    

    Any data that needs to be passed between the host application and module needs to inherit from MarshalByRefObject so your Person class should look something like this if you intend to pass it back and forth.

    public class Person : MarshalByRefObject
    {
        ...
    }
    

    Now, regarding persisting the state of Person even though new attributes have been added.

    Since your host application doesn't need to know about these new attributes, you should move the persistent of these types of objects into a ModuleManager which lives inside the AppDomain for the plugin and expose some methods on your IModule that know how to do the actual persistence.

    public interface IModule
    {
        Person GetPerson();
        void Print();
    
        IDictionary<string, object> GetState();
        void SetState(IDictionary<string, object> state);
    }
    
    public class ModuleManager : MarshalByRefObject
    {
        public IModule Module { get; set; }
    
        public void Initialize(string path)
        {
            // Load the module
            // Setup file watcher
        }
    
        public void SaveState(string file)
        {
            var state = this.Module.GetState();
            // Write this state to a file
        }
    
        public void LoadState(string file)
        {
            var state = this.ReadState(file);
            this.Module.SetState(state);
        }
    
        private IDictionary<string, object> ReadState(string file)
        {
            ...
        }
    }
    

    The ModuleManager should be responsible for saving the state of the module so each module developer doesn't have to implement this functionality. All it has to do is read and write it's state to and from a dictionary of key/value pairs.

    You will find this approach manageable rather than trying to perform all the persistence in your host application.

    The idea here is marshal as little data as possible between the host app and plugin. Each plugin knows what fields are available in Person so let them be responsible for indicating what needs to be persisted.