2009-05-06 10 views
15

Ho diviso il mio problema in una versione breve e lunga per le persone con poco tempo a disposizione.Architettura plug-in C# con interfacce condivise tra i plugin

Versione corta:

Ho bisogno di un po 'di architettura per un sistema con il fornitore e dei consumatori plugin. I provider dovrebbero implementare l'interfaccia IProvider ei consumatori dovrebbero implementare IConsumer. L'applicazione in esecuzione dovrebbe essere a conoscenza solo di IProvider e IConsumer. Un'implementazione consumer può chiedere all'assembly in esecuzione (tramite un ServiceProcessor) quali provider implementano InterfaceX e ottiene un elenco indietro. Questi oggetti IProvider devono essere castati su InterfaceX (nel consumer) per poter agganciare il consumatore ad alcuni eventi definiti da InterfaceX. Ciò fallirà perché l'assembly in esecuzione in qualche modo non conosce questo tipo di InterfaceX (cast fallisce). La soluzione sarebbe includere InterfaceX in un assembly che sia il plug-in che il riferimento di assembly in esecuzione, ma questo dovrebbe significare una ricompilazione per ogni nuova coppia di provider/consumer ed è altamente indesiderabile.

Qualche suggerimento?

Versione lunga:

che sto sviluppando una sorta di servizio generico che utilizzerà i plugin per il raggiungimento di un più alto livello di riutilizzabilità. Il servizio consiste in una sorta di implementazione del pattern Observer usando Provider e Consumatori. Sia i provider che i consumatori dovrebbero essere plug-in per l'applicazione principale. Permettetemi innanzitutto di spiegare come funziona il servizio elencando i progetti che ho nella mia soluzione.

Progetto A: un progetto di servizio Windows per l'hosting di tutti i plug-in e funzionalità di base. Un progetto Windows Form di TestGUI viene utilizzato per semplificare il debug. Un'istanza della classe ServiceProcessor del progetto B sta eseguendo il plugin. Le sottocartelle "Consumatori" e "Provider" di questo progetto contengono sottocartelle in cui ogni sottocartella contiene rispettivamente un plug-in del provider o del provider.

Progetto B: una libreria di classi che contiene la classe ServiceProcessor (che esegue tutti i plug-in di caricamento e dispatch tra plug-in, ecc.), IConsumer e IProvider.

Progetto C: una libreria di classi, collegata al progetto B, costituita da TestConsumer (che implementa IConsumer) e TestProvider (che implementa IProvider). Un'interfaccia aggiuntiva (ITest, derivata da IProvider) è implementata dal TestProvider.

L'obiettivo qui è che un plug-in Consumer può chiedere al ServiceProcessor quali Provider (che implementano almeno IProvider) che ha). Gli oggetti IProvider restituiti devono essere inoltrati all'altra interfaccia implementata (ITest) nell'implementazione IConsumer in modo che l'utente possa collegare i gestori eventi agli eventi ITest.

All'avvio del progetto A, vengono caricate le sottocartelle contenenti i plug-in consumer e provider. Di seguito sono riportati alcuni problemi che ho riscontrato finora e ho cercato di risolvere.

L'interfaccia ITest utilizzata per risiedere nel Progetto C, poiché ciò si applica solo ai metodi e agli eventi di cui TestProvider e TestConsumer sono a conoscenza. L'idea generale è di mantenere il progetto A semplice e inconsapevole di ciò che i plugin fanno l'uno con l'altro.

Con ITest nel progetto C e codice nel metodo Initialize del TestConsumer che esegue l'IProvider su ITest (questo non dovrebbe fallire in una singola libreria di classi stessa quando un oggetto che implementa ITest è noto come oggetto IConsumer) non valido si verificherebbe un errore di trasmissione.Questo errore può essere risolto posizionando l'interfaccia ITest nel progetto B a cui fa riferimento anche il progetto A. È tuttavia molto indesiderato poiché è necessario ricompilare il progetto A quando viene creata una nuova interfaccia.

Ho provato a mettere ITest in una libreria a classe singola a cui fa riferimento solo il progetto C, poiché solo il fornitore e il consumatore devono essere a conoscenza di questa interfaccia, ma senza successo: quando carica il plugin il CLR afferma che il progetto di riferimento potrebbe non essere trovato Questo potrebbe essere risolto agganciando l'evento AssemblyResolve dell'attuale AppDomain ma in qualche modo ciò sembra indesiderato. ITest è tornato di nuovo al Progetto B.

Ho provato a suddividere il progetto C in un progetto separato per l'utente e il provider e caricare entrambi gli assembly che funzionano correttamente: entrambi gli assembly sono residenti nella raccolta Assemblies o nell'AppDomain corrente: Assembly trovato: Datamex.Projects. Polaris.Testing.Providers, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = 2813de212e2efcd3 montaggio trovati: Datamex.Projects.Polaris.Testing.Consumers, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = ea5901de8cdcb258

Dal momento che il Consumatore utilizza il Fornitore, è stato fatto un riferimento dal Consumatore al Fornitore. Ora l'evento AssemblyResolve sparò di nuovo affermando di cui ha bisogno il seguente file: AssemblyName = Datamex.Projects.Polaris.Testing.Providers, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = 2813de212e2efcd3

Le mie domande: perché è questo ? Questo file è già caricato correttamente? Perché il cast di IProvider su qualche interfaccia so che implementa impossibile? Ciò è probabilmente dovuto al fatto che il programma in esecuzione non conosce questa interfaccia, ma non può essere caricato in modo dinamico?

Il mio obiettivo finale: I plug-in consumer chiedono al ServiceProcessor che Provider ha implementato l'interfaccia x. I provider possono essere convertiti in questa interfaccia x, senza che l'assembly sia a conoscenza dell'interfaccia x.

Qualcuno che può aiutare?

Grazie in anticipo, Erik

+4

A rischio di sembrare glib, è necessario provare a rendere questa domanda più breve. –

+0

Woah ... d'accordo! – Svish

+0

Ho aggiunto una versione breve in cima alla versione lunga :-) Scusate per il post lungo ma è abbastanza difficile descrivere il mio problema. –

risposta

14

Ho appena provato a ricreare la soluzione nel miglior modo possibile e non ho tali problemi. (Attenzione, un sacco di esempi di codice seguono ....)

primo progetto è l'applicazione, questa contiene una classe:

public class PluginLoader : ILoader 
{ 
    private List<Type> _providers = new List<Type>(); 

    public PluginLoader() 
    { 
     LoadProviders(); 
     LoadConsumers(); 
    } 

    public IProvider RequestProvider(Type providerType) 
    { 
     foreach(Type t in _providers) 
     { 
      if (t.GetInterfaces().Contains(providerType)) 
      { 
       return (IProvider)Activator.CreateInstance(t); 
      } 
     } 
     return null; 
    } 

    private void LoadProviders() 
    { 
     DirectoryInfo di = new DirectoryInfo(PluginSearchPath); 
     FileInfo[] assemblies = di.GetFiles("*.dll"); 
     foreach (FileInfo assembly in assemblies) 
     { 
      Assembly a = Assembly.LoadFrom(assembly.FullName); 
      foreach (Type type in a.GetTypes()) 
      { 
       if (type.GetInterfaces().Contains(typeof(IProvider))) 
       { 
        _providers.Add(type); 
       } 
      } 
     } 

    } 

    private void LoadConsumers() 
    { 
     DirectoryInfo di = new DirectoryInfo(PluginSearchPath); 
     FileInfo[] assemblies = di.GetFiles("*.dll"); 
     foreach (FileInfo assembly in assemblies) 
     { 
      Assembly a = Assembly.LoadFrom(assembly.FullName); 
      foreach (Type type in a.GetTypes()) 
      { 
       if (type.GetInterfaces().Contains(typeof(IConsumer))) 
       { 
        IConsumer consumer = (IConsumer)Activator.CreateInstance(type); 
        consumer.Initialize(this); 
       } 
      } 
     } 
    } 

Ovviamente questo può essere riordinato enormemente.

prossimo progetto è la libreria condivisa che contiene i seguenti tre interfacce:

public interface ILoader 
{ 
    IProvider RequestProvider(Type providerType); 
} 

public interface IConsumer 
{ 
    void Initialize(ILoader loader); 
} 

public interface IProvider 
{ 
} 

Infine c'è il progetto di plugin con queste classi:

public interface ITest : IProvider 
{   
} 

public class TestConsumer : IConsumer 
{ 
    public void Initialize(ILoader loader) 
    { 
     ITest tester = (ITest)loader.RequestProvider(typeof (ITest)); 
    } 
} 

public class TestProvider : ITest 
{   
} 

Sia l'applicazione che i progetti plug riferimento alla progetto condiviso e la DLL del plugin viene copiata nella directory di ricerca dell'applicazione, ma non si riferiscono tra loro.

Quando il PluginLoader è costruito, trova tutti gli IProviders quindi crea tutti gli ICon e le chiamate Inizializza su di essi. All'interno dell'inizializzazione il consumatore può richiedere i provider dal caricatore e nel caso di questo codice viene costruito e restituito un TestProvider. Tutto questo funziona per me senza controllo di fantasia del caricamento degli assiemi.

+0

Grazie mille. Mi tufferò in questo e ti farò sapere! –

+0

Ho provato il tuo codice e funziona. Grazie ancora. Tuttavia non differisce molto da quello che ho già fatto (che non funziona). Pubblicherò la soluzione che ho realizzato e forse riuscirai a individuare il problema dal momento che ancora non avrò la minima idea :-) –

+0

Alla fine ho capito cosa ho fatto di sbagliato. Era solo UN unico metodo che ha fatto la differenza tra una soluzione di lavoro e alcune ore o il debug: Il codice di caricamento dell'Assembly: Assembly a = Assembly.LoadFrom (assembly.FullName); Il mio errato codice di caricamento era: Assembly assembly = Assembly.LoadFile (filename); Quando ho cambiato questo in LoadFrom tutto ha funzionato bene !! Grazie ancora per il tuo tempo! –

0

Se sei domanda è: come possono due gruppi indipendenti di condividere la stessa interfaccia, la risposta è 'non si puo' Una soluzione è quella di includere l'interfaccia in tutte le assemblee, forse in una DLL che i plugin-builder possono fare riferimento e nel proprio assembly di caricamento.

+0

La domanda è in realtà: come possono 2 assembly correlati (provider e consumer) condividere un'interfaccia (tramite una classe lib separata quando necessario) che essa stessa deriva da un'interfaccia che l'assembly in esecuzione conosce. L'assemblea in esecuzione sa solo che ci sono fornitori e consumatori ma non sa di avere un contratto più dettagliato tra loro. –

0

Ho fatto qualcosa di simile a quello che stai cercando di fare e finché ho avuto gli assembly in un posto in cui il caricatore sembrava automaticamente, non ho riscontrato alcun problema.

Hai provato a mettere tutti i tuoi assemblaggi in una sottodirectory in cui si trova l'exe? Non riesco a ricordare i dettagli ora ma c'è una lista di passaggi documentati su dove esattamente e in quale ordine il caricatore cerca gli assemblaggi/tipi.

+0

Hmm questo potrebbe essere qualcosa. I plugin non si trovano in una sottocartella dell'assembly in esecuzione ma in cartelle che possono essere configurate. L'assembly in esecuzione carica sia plug-in provider che consumer utilizzando Assembly.LoadFile() (e ho verificato che si trovino nell'AppDomain corrente). In qualche modo, un cast da IProvider a IMoreDetailedProvider non può essere creato nel consumer, tranne quando questo IMoreDetailedProvider risiede in una libreria di classi separata a cui fa riferimento l'assembly in esecuzione. –

+0

Ecco perché penso che sia probabilmente un problema di caricatore. Inoltre, ricontrolla la tua specifica di tipo ovunque ti trovi. – JohnOpincar

4

È ancora in fase di sviluppo, ma suona come un caso perfetto per MEF (da includere in .Net 4) e utilizzato internamente in VS2010.

MEF presenta una soluzione semplice per il problema di estensibilità di runtime . Fino al ora, qualsiasi applicazione che desiderava supportare un modello di plugin necessario per crea la propria infrastruttura da zero . Tali plug-in sarebbero spesso specifici dell'applicazione e non potevano essere riutilizzati in più implementazioni .

Anteprime sono già disponibili sul http://www.codeplex.com/MEF

La blog di Glen blocco può anche essere utile.

+0

Stavo per rispondere che avrebbe dovuto semplicemente leggere su MEF, quindi ho semplicemente votato la tua risposta. MEF risolve la logistica di affrontare tutto questo e i loro documenti e articoli su di esso coprono le teorie su come definire e condividere concetti come Interfacce. Suggerisco caldamente di guardare al MEF e non costruirne uno personale. –