Laden...

Wie Dependency Injection registrieren bei Plugins, die erst zur Laufzeit bekannt sind?

Erstellt von -val- vor 4 Jahren Letzter Beitrag vor 4 Jahren 1.626 Views
-
-val- Themenstarter:in
3 Beiträge seit 2019
vor 4 Jahren
Wie Dependency Injection registrieren bei Plugins, die erst zur Laufzeit bekannt sind?

Hallo,

ich beschäftige mich mit Dependency Injection und habe da ein Problem...
Es wird empfohlen alle Abhängikgeiten in einer Root-Klasse zu registrieren. Wie soll das z.B. bei Verwendung von Plugins funktionieren, da ich erst zur Laufzeit weiß, was geladen wird.
Zum Beispiel:

AppHost.exe (WPF Anwendung)
PluginA.dll
PluginB.dll
PluginC.dll

AppHost.exe => Läd PluginA.dll => PluginA.dll kann je nach Benutzerauswahl PluginB.dll oder PluginC.dll laden. In PluginB.dll werden nun weitere Abhänigkeiten benötigt oder erzeugt usw.
Wie kriege ich nun den ServiceProvider nach PluginB bzw. PluginC? Ich könnte den ServiceProvider nun über den Konstruktor oder einer Initialisierungs-Methode durch reichen, aber dann kann ich gleich einen Singleton verwenden. Desweiteren wird dieser Weg auch nicht empfohlen. Alternative wäre der ServiceLocater. Das ist wiederum ein Singleton.

Hat jemand in dieser Thematik Erfahrung und kann empfehlen, wie man dieses Problem am besten angeht? Danke.

5.657 Beiträge seit 2006
vor 4 Jahren

Ich würde es wahrscheinlich so umsetzen, daß jedes Plugin eine Methode hat, die die jeweiligen Abhängigkeiten zurückgibt. Der PluginManager (oder wie man das auch nennt) sammelt die Abhängigkeiten und lädt sie dann in der korrekten Reihenfolge.

Weeks of programming can save you hours of planning

-
-val- Themenstarter:in
3 Beiträge seit 2019
vor 4 Jahren

Müsste der PluginManager dafür nicht alle Plugins kennen? Bei z.B. 100 Plugins müssten anschließend unnötig alle Plugins initialisiert werden. Was ist mit den Plugins welche später zur Laufzeit hinzugeladen werden?

5.657 Beiträge seit 2006
vor 4 Jahren

Nein, der PluginManager bekommt die Abhängigkeiten mitgeteilt, wenn er ein Plugin lädt.

Die Abhängigkeiten kann das Plugin beim Laden mitteilen, oder z.B. auch in einer Konfigurationsdatei hinterlegen, so daß sie schon vor dem eigentlichen Laden bekannt sind.

Weeks of programming can save you hours of planning

2.078 Beiträge seit 2012
vor 4 Jahren

Du gibst beim Start nicht die Instanzen der Abhängigkeiten an, sondern nur die Info, wie man die Abhängigkeit instanziiert 😉
Natürlich müssen dafür alle DLLs geladen werden, aber das sollte im Zweifel kein Problem darstellen. Wenn Du das trotzdem nicht möchtest, kannst Du auch eine Factory nutzen, die dann die DLL sowie die gesuchte Implementierung zur Laufzeit lädt und instanziiert.

Ausführlicher beschäftigt habe ich mich bisher mit folgenden DI-Frameworks:

  • Microsoft.Extensions.DependencyInjection
  • Autofac
  • DryIoc

Alle drei Frameworks können Abhängigkeiten entsprechend verwalten und instanziieren (richtig angewandt) auch nur die, die am Ende wirklich abgefragt werden.

Dabei ist natürlich wichtig, dass alle möglichen Abhängigkeiten zum Beginn der Anwendung entsprechend registriert werden. Dabei wird nichts instanziiert, es wird nur der Typ mitgeteilt.
Das Framework merkt sich dann jeden Typ, dessen Abhängigkeiten und noch zusätzliche Dinge, z.B. ob und wie lange eine Instanz vorgehalten werden soll.
Wird ein Typ angefragt, löst es diese Abhängigkeiten auf und instanziiert entsprechend der Einstellungen alles andere.

Einem Plugin-Loader würde ich dann die Möglichkeit geben, eigene Typen zu registrieren und zum Schluss registriert es dann das eigentliche Plugin.

Am Einfachsten zum Lernen dürfte die Variante von Microsoft sein, Autofac ist aber ebenfalls recht einfach zu lernen und kann etwas mehr. Bei DryIoc ist mMn. der Einstieg detlich schwieriger, allerdings kann es sehr viel, bleibt dabei aber trotzdem sehr performant.
Die beiden sind aber auch voll-kompatibel zu der Variante von Microsoft, Du kannst also überall die Microsoft-Variante nutzen, nur beim initialisieren nutzt Du dann die Implementierung des jeweiligen Frameworks.

-
-val- Themenstarter:in
3 Beiträge seit 2019
vor 4 Jahren

Danke für die Hinweise, ich konnte nun das Problem weiter abstrahieren...
Wie kriegt man am sinnvollsten den PluginProvider, PluginAssemblyProvider usw. in einem anderen Plugin bekannt?

Wäre das eine Möglichkeit? Problem nur, es passiert nicht "automatisiert".


// Übergabe der Abhängigkeit
// mainPlugin.Initialize(serviceProvider.GetService<PluginProvider>());


    public interface IPlugin : IDisposable
    {
        event EventHandler<string> Disposed;
        IPluginInfo Info { get; }        
        T GetContract<T>() where T : class;
        
        void Initialize(object initializeObject);
    }


public interface ICommandPlugin : IPlugin
    {
        IPluginCommand Command { get; }        
    }



// Startup.dll
public class Startup : CommandPlugin, ICommandStart
    {
        IPlugin mainPlugin;
    
        public override void Initialize(object initializeObject)
        {
            var serviceCollection = new ServiceCollection();
            
            serviceCollection.AddTransient((s) => new PhysicalFileProvider("\\Plugins", "*.dll", true, true));
            serviceCollection.AddTransient<IPluginProvider<PluginAssembly>, PluginAssemblyProvider>();
            serviceCollection.AddTransient<IPluginProvider<IPlugin>, PluginProvider>();

            var serviceProvider = serviceCollection.BuildServiceProvider();

            mainPlugin = serviceProvider.GetService<PluginProvider>().CreatePlugin<ICommandPlugin>("MP01"); // MainPlugin

            // Übergabe der Abhängigkeit
            // mainPlugin.Initialize(serviceProvider.GetService<PluginProvider>());            
            mainPlugin.Command.Execute<ICommandStart>(this);
        }    

        public void Start(IPlugin sender, params object[] parameters)
        {
            mainPlugin.Command.Execute<ICommandStart>(this);
        }
    }
}


// MainPlugin.dll
public class MainPlugin: CommandPlugin, ICommandStart
    {            
       PluginProvider pluginProvider;

       public override void Initialize(object initializeObject)
       {
          if (initializeObject is PluginProvider pluginProvider)
          {
               // this.pluginProvider = pluginProvider;
          }
       }

       public void Start(IPlugin sender, params object[] parameters)
       {
            // 
       }
    }
}

16.806 Beiträge seit 2008
vor 4 Jahren

Es ist ungünstig, dass die Plugins eine direkte Abhängigkeit zur Implementierung haben.
Es reicht doch, dass die Plugins das Interface kennen; also zB IPluginProvider

Wenn man sich hier an die Namespace Regeln von .NET hält, dann kann man das auch super einfach über ein entsprechendes Mono Repo in Form von mehreren Projekten abdecken, sodass es keine Circular Dependencies gibt.
Man kann sich hier einfach den Aufbau abschauen, wie es die Microsoft.Extensions.* NuGets machen.

So wie ich das sehe leben Deine Plugins zum einen eingbettet aus der Assembly, zum anderen aus Dateien - und Du hast dafür verschiedene Provider.
Das macht Dein DI Design unnötig komplex, soweit ich das sehe; und entspricht auch nicht den Empfehlungen von DI (zumindest das Microsoft DI).
Hab lieber ein Provider, der aber aus verschiedenen Quellen laden kann, wie es auch gängige Bibliotheken tun (ASP.NET, MediatR, EFCore....).

Bei Dir könnte das so aussehen:

serviceCollection.AddPluginContext<PluginContext>>(o=>
{
   o.UsePluginsFromAssembly();
   o.UsePluginsFromAssembly(typeof(MyCustomType).Assembly);
   o.UsePluginsFromPhysicalDirectory("\\Plugins", "*.dll", true, true);
});

Dazu brauchst Du eine Options Klasse, die Du dann als Action verwenden kannst:


        public static IServiceCollection AddPluginContext<TContext>(
            this IServiceCollection services, Action<PluginContextOptions> options)
            where TContext : class, IPluginContext
        {
            PluginContextOptions contextOptions = new PluginContextOptions();
            options.Invoke(contextOptions);

            services.Configure<PluginContextOptions>(contextOptions);
            services.AddScoped<IPluginContext, TContext>();

            // ....
            return services;
        }