Laden...

Dynamischen PluginSystem mit Reflections und AppDomains

Erstellt von Yothri vor 8 Jahren Letzter Beitrag vor 8 Jahren 1.810 Views
Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren
Dynamischen PluginSystem mit Reflections und AppDomains

Hallo,

ich bin derzeitig dabei ein Plugin System zu schreiben, welches die Möglichkeit haben soll, Plugins während der Laufzeit zu laden und auch zu entladen. Das ganze hab ich dann der Schönheit halber auch direkt generisch gemacht, man kann das PluginInterface also selbst noch angeben. Schön und gut, da bin ich dann auch schon auf das erste Problem gestoßen.

Ich hatte mir das so gedacht, neben der Default-AppDomain läuft noch eine weitere (Plugin)-Domain in der die Plugins geladen werden. Möchte ich die Plugins neuladen, entlade ich die Plugin-Domain und erstelle eine neue und lade dort dann die Plugins wieder (neu) rein.

Zwischendurch ist mir dann die Überlegung gekommen, dass es doch toll wäre, wenn man jedes Plugin dann noch einzeln entladen bzw. laden könnte. Dafür müsste dann jedes Plugin seine eigene App-Domain haben. Ist das sinnvoll?

Das ist aber nicht mein Hauptproblem,
mein Hauptproblem ist ein anderes. Ich habe mir meine Struktur in etwa so gedacht:

Ich habe eine Haupt-Applikation (Host-Application) welche eine ganz kleine Bibliothek referenziert, nämlich das PluginInterface damit der PluginController (die Klasse, die alle Plugins nachher verwaltet) den Typ der einzelnen Plugins weiß.
Zusätzlich referenziert die Haupt-Applikation noch eine andere Bibliothek die allgemeine Funktionen beinhaltet.

Im Debug Ordner meiner Applikation ist ein Ordner "plugins" wo dann alle Plugins drin liegen.

Und nun zum Problem, jedes Plugin referenziert ebenso die PluginInterface Bibliothek damit das Plugin weiß, wie es auszusehen hat. Zusätzlich kann das Plugin optional auch noch die Bibliothek referenzieren die allgemeine Funktionen beinhaltet (genau wie die Hauptapplikation) das ist aber wie gesagt eher optional.

Wenn ich jetzt mit einem Objekt der PluginController Klasse versuche das "plugins" Verzeichnis auszulesen und für jedes darin enthaltene Plugin eine AppDomain erstelle, kommt bei der Load() Methode eine Exception, dass die Assembly nicht geladen werden kann, weil wahrscheinlich Verweise fehlen was ja auch klar ist, denn die PluginInterface-Assembly ist ja nicht in der erstellten AppDomain und die andere optionale Bibliothek mit den allgemeinen Funktionen auch nicht.

Ich denke das ich hier einen Strukturfehler habe, was meint ihr?

Ich wäre euch sehr dankbar für ein paar Ratschläge und Tipps bezüglich AppDomains. Vielleicht findet sich ja auch eine einfache Lösung für das Problem.

Danke im Voraus

Mit freundlichen Grüßen
Yothri

16.833 Beiträge seit 2008
vor 8 Jahren

Bezüglich AppDomain und dessen Zukunft siehe mein Hinweis in
Assembly inkl Referenzen dynamisch laden

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Hallo Abt,

danke für deinen Hinweis, ist mein Vorhaben denn mit der Abschaffung von der AppDomain im neuen .NET Core denn überhaupt so möglich? Wenn ja, dann wäre es natürlich perfekt, wenn es dazu schon Literatur gibt. Hättest du da was zu empfehlen?

An alle anderen, ich habe mich bezüglich des neuen .NET Core's noch nicht entschieden und bin daher auch nach wie vor für Lösungen im beschriebenen Problem offen.

Mit freundlichen Grüßen
Yothri

16.833 Beiträge seit 2008
vor 8 Jahren

.NET Core deckt nur - deswegen der Name - Core-Features ab. Es ist nicht mal fertig, weshalb es auch nicht wirklich Literatur dazu gibt.
System.Addin ist (noch) nicht Bestandteil des .NET Cores - wird es vielleicht auch nie.

Wie genau das Nachfolgersystem von .NET Framework 4.6 aussieht, ob es ein .NET Framework 5 geben wird - oder ob es .NET Core mit NuGet ist: aktuell nicht bekannt.
.NET steht vor einem enormen Umbruch.

Ohne AppDomain ist das Entladen einer DLL aktuell auch nicht möglich.
Es war lediglich ein Hinweis, dass Du das in Deiner Evaluierung beachten solltest, dass die Zukunft der AppDomain nicht ganz klar ist.

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Zur Kenntnis genommen, ich werde mich dann weiter auf die AppDomain konzentrieren. Trotzdem danke 😃

Edit:

Ich habe es fast geschafft, ich hab die ganze Zeit versucht aus dem plugins ordner das Plugin zu laden. Das ging aber nicht, keine Ahnung warum, Assembly.LoadFrom funktioniert nur wenn ich das Plugin in den selben Ordner lege wie die Host -App.

Ich machs jetzt wie folgt: Es gibt eine Plugin Domain die mit AppDomainSetup und dessen Property Codebase = Path.Combine(Environment.CurrentDirectory, "plugins") initialisiert wird. Das sollte der Domain doch sagen, dass Assemblies nicht im Root ordner suchen soll sondern im plugins ordner.
Assembly.LoadFrom lädt anscheinend zwar das plugin ausm plugins ordner, aber das plugin muss auch im root ordner liegen sonst wirft LoadFrom() eine exception.
Sehr komisches Verhalten. Wenn das gefixt ist, bin ich zufrieden. Weiß einer um Rat?

Hinweis von Abt vor 8 Jahren

Bitte keine Full-Quotes mehr
[Hinweis] Wie poste ich richtig? Punkt 2.3

Mit freundlichen Grüßen
Yothri

446 Beiträge seit 2004
vor 8 Jahren

hi,

welche Exception wird geworfen? Bei AppDomains ist es immer wichtig, dass du angibst aus welcher AppDomain der Code ausgeführt werden soll. Wenn du nichts angibst, wird der Code in der DefaultAppDomain exekutiert.

Schau dir vlt. mal folgedes Beispiel an.

Schaut mal im IRC vorbei:
Server: https://libera.chat/ ##chsarp

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Schau dir vlt. mal folgedes
>
an.

Also ich hab mir dein Beispiel mal angeschaut und meine Klasse dann etwas abgeändert.
Ich hatte es vorher schon einmal versucht, aber das DoCallBack() wirft mir eine SerializationException wenn ich darin

Assembly.Load();

aufrufe.

Gibt auch komischerweise nicht so viel im Netz zu der Exception bezüglich AppDomains.
Und ja, habe gemerkt, dass die Plugins wie ich es vorher hatte in die Default-AppDomain geladen wurden was ich ja natürlich nicht will.

Ich habe hier mal meine Klasse reinkopiert:

public class PluginController<T> where T : class
    {

        public IEnumerable Plugins { get; set; }

        private AppDomain pluginDomain;
                
        public PluginController()
        {
            Plugins = new List<T>();
        }

        public void LoadPlugins()
        {
            if(pluginDomain == null)
                pluginDomain = AppDomain.CreateDomain("PluginDomain");
            
            (Plugins as List<T>).Clear();

            string[] files = Directory.GetFiles(Environment.CurrentDirectory, "*.dll");
            for(int i = 0; i < files.Length; i++)
            {
                string plugin = files[i];
                var pluginProxy = pluginDomain.CreateInstanceAndUnwrap(typeof(PluginProxy).Assembly.FullName, typeof(PluginProxy).FullName) as PluginProxy;
                Assembly pluginAssembly = pluginProxy.LoadPlugin(plugin);
                ProcessPluginAssembly(pluginAssembly);
            }

            Assembly[] currentDomainAssemblies = AppDomain.CurrentDomain.GetAssemblies();
            Assembly[] pluginDomainAssemblies = pluginDomain.GetAssemblies();

            Console.WriteLine();
            Console.WriteLine("Current Domain Assemblies");
            Console.WriteLine();
            foreach(Assembly assembly in currentDomainAssemblies)
                Console.WriteLine(assembly.GetName().Name);

            Console.WriteLine();
            Console.WriteLine("Plugin Domain Assemblies");
            Console.WriteLine();
            foreach(Assembly assembly in pluginDomainAssemblies)
                Console.WriteLine(assembly.GetName().Name);
                        
            Logger.Log("Plugin Manager", "{0} plugins loaded.", Logger.LogLevel.Info, (Plugins as List<T>).Count);
        }

        private void ProcessPluginAssembly(Assembly pluginAssembly)
        {
            foreach(var type in pluginAssembly.GetTypes())
            {
                if(type.IsInterface || type.IsAbstract)
                    continue;
                else
                {
                    if(type.GetInterface(typeof(T).FullName) != null)
                    {
                        T plugin = Activator.CreateInstance(type) as T;
                        (Plugins as List<T>).Add(plugin);
                    }
                }
            }
        }

        public void UnloadPlugins()
        {
            if(pluginDomain != null)
            {
                AppDomain.Unload(pluginDomain);
                pluginDomain = null;
            }
        }

        private class PluginProxy : MarshalByRefObject
        {

            public Assembly LoadPlugin(string assemblyPath)
            {
                return Assembly.Load(File.ReadAllBytes(assemblyPath));
            }

        }

    }

Mithilfe der Konsolenausgabe erkenn' ich, dass die Plugins sowohl in der pluginDomain als auch in der Default Domain sind. Das kann ich mir überhaupt nicht erklären.

Mit freundlichen Grüßen
Yothri

16.833 Beiträge seit 2008
vor 8 Jahren

Kann das sein, dass Deine Plugin-DLLs andere DLLs referenzieren und brauchen?
Die müssen natürlich im entsprechenden Ordner liegen und womöglich sind die bei Dir einfach im Hauptordner.

Jede AppDomain muss alles nötige laden.

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Kann das sein, dass Deine Plugin-DLLs andere DLLs referenzieren und brauchen?
Die müssen natürlich im entsprechenden Ordner liegen und womöglich sind die bei Dir einfach im Hauptordner.

Ja, also ich lade jetzt zu Testzwecken zwei Plugins aus dem Hauptordner. Beide Plugins referenzieren die selben Assemblies welche auch im Hauptordner liegen. Aber was komisch ist, plugin1 referenziert BDShared und PluginInterface. plugin2 auch.

Und in der AppDomain ist BDShared und PluginInterface dann doppelt ?!

Alles total komisch und verwirrend.

Aber warum beide AppDomains jetzt beide Plugins laden, weiß ich absolut nicht. Ich lade nämlich eigentlich nur in die pluginDomain wie man dem Code ja auch entnehmen kann.

Mit freundlichen Grüßen
Yothri

16.833 Beiträge seit 2008
vor 8 Jahren

Die Idee ist: jede AppDomain ist ein in sich komplett isolierter Container.
Elemente, die Du in AppDomain A lädst sind in AppDomain B nicht bekannt. So kannst Du Abhängigkeiten laden, die sich komplett unterscheiden (zB. verschiedene Version einer externen DLL je AppDomain).

Nur eine AppDomain für alle Plugins zu machen ist aus diesen Gründen auch etwas sinnfrei.
Du kennst die Abhängigkeiten eines Plugins nicht.
Wenn PluginA DllX in der Version1 referenziert und PluginB die DllX in Version2 referenziert, dann explodiert Deine gesamte AppDomain.

DLLs können nicht entladen werden. Das kann .NET nicht. Das hat konzeptionelle Gründe.
Wenn Du Plugins entladen willst, dann packt man sie in eine AppDomain und entlädt die entsprechende AppDomain.
Bei Dir hätte das die Folge, dass man alle Plugins entlädt.
Das ist nicht der Zweck.

Was genau bei Dir krum ist seh ich jetzt auf Anhieb nicht. Vielleicht löst sich das auch von ganz allein, wenn Du die eigentliche Idee des Konzepts - getrennte AppDomains - verfolgst.
Es ist auf alle Fälle nicht sinnvoll, dass Du LoadPlugin nicht in einen try-catch packst.
Auch untypisierte Dinge wie public IEnumerable Plugins { get; set; } gehen gar nicht. Das muss List<T> heissen, wie beim instantiieren..
Du hast meiner Meinung nach auch ein paar konzeptionelle Fehler, wie zB. ein gemeinsames Interfaces zB. IPlugin fehlt in Deinem Proxy.
Das aber nur nebenbei.

Ich hab das schon ewig nicht mehr direkt gemacht, sondern meistens MEF verwendet.
Gib Dir mal beim Output auch aus, wie der FriendlyName der AppDomains jeweils ist.

Les Dir mal ddas durch:
C# Plugins mit AppDomains realisieren

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Ja, die Idee eine AppDomain für jedes Plugin zu machen kam mir auch schon (wie weiter oben erwähnt). Ich werde es dann wohl so umsetzen, macht ja auch viel mehr Sinn.

Das mit dem IPlugin versteh ich nicht so ganz. Die Proxy Klasse ist doch in der PluginController Klasse drin, reicht das nicht?

Ich würde auch lieber auf eine Proxy Klasse verzichten und das ganze mit dem domainPlugin.DoCallBack() machen, aber wenn ich darin versuche eine Assembly zu laden, wirft das eine SerializationException. Warum? Absolut keine Ahnung.

Mit freundlichen Grüßen
Yothri

16.833 Beiträge seit 2008
vor 8 Jahren

Schau Dir bitte den Link an. Da werden Deine Fragen auch beantwortet.

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Ich kenne den Link schon, das hab ich alles schon mal versucht durchzuarbeiten.

Ein Gedanke der mir auch nicht aus dem Kopf verschwindet ist: Ich durchlaufe alle DLL-Dateien in meinem plugins Ordner. Manche davon sind aber keine Plugins sondern eben Bibliotheken die von dem Plugins nur referenziert werden. Um das rauszufinden muss ich die DLL aber doch erstmal laden um zu gucken, ob das Interface implementiert wird. Das würde aber auch bedeuten, dass selbst die referenzierten Assemblies die garkeine Plugins sind in ne eigene AppDomain geladen werden.

Edit: Hier noch ein Bild. Ich kapiers nicht, was soll diese dämliche SerializationException.

Mit freundlichen Grüßen
Yothri

16.833 Beiträge seit 2008
vor 8 Jahren

..deswegen betten Plugin-DLLs oft externe Bibliotheken zB. durch ILmerge ein.
DLLs, die Dein Interface wie IPlugin nicht implementieren, sind kein Plugin, werden nicht geladen und erhalten keine AppDomain.

Du stellst typische Fragen, die durch übliche Lektüre und BLogs bzgl. Best Practise beantwortet werden.
Nur so als Hinweis 😉

Deinem vorherigen Code ist nicht zu entnehmen, dass Du meinen Link bereits kanntest.
Wie erwähnt; Du beachtet IPlugin als gemeinsames Interface nicht. Du checkst nur den Proxy.
Das macht der Link anders.

Ich antworte Dir nicht, um zu schaden - sondern zu helfen...
Hast Du mal nach entsprechender Fehlermeldung Google befragt? Dazu findet man zig Gründe und entsprechende Lösungen.

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Danke, ich versteh jetzt, das ich Variablen z.B. von der CurrentDomain nicht einfach in der pluginDomain abrufen kann.

Ich werde jetzt erstmal weiter rum probieren bevor ich mich hier völlig lächerlich mache.
Danke sehr 😃

Mit freundlichen Grüßen
Yothri

16.833 Beiträge seit 2008
vor 8 Jahren

Deswegen hab ich betont, dass es vollständig isolierte Container sind.
Lächerlich macht sich hier niemand =)

Y
Yothri Themenstarter:in
20 Beiträge seit 2015
vor 8 Jahren

Heikles Thema. Die Fehlermeldungen sind aber wirklich irreführend.
Das ist mir alles zu kompliziert, ich hab mir das viel leichter vorgestellt. Ich denke ich schau mir mal CS-Script an. Wenn das alles zur Laufzeit funktioniert, Scripts laden und entladen alles während meine Host Applikation läuft dann reicht mir das sogar aus. Wäre sogar viel besser wenn ich so darüber nachdenke.
Trotzdem danke für die Mühe.

Mit freundlichen Grüßen
Yothri