Managed Extensibility Framework (MEF)

.NET Framework (current version)
 

In questo argomento viene fornita una panoramica di Managed Extensibility Framework, introdotto in .NET Framework 4.

Managed Extensibility Framework o MEF è una libreria per la creazione di applicazioni leggere ed estendibili. Consente agli sviluppatori di applicazioni di scoprire e usare le estensioni senza alcuna configurazione. Consente anche agli sviluppatori di estensioni di incapsulare facilmente il codice ed evitare dipendenze rigide fragili. Con MEF è possibile riutilizzare le estensioni non solo all'interno ma anche tra le applicazioni.

Si immagini di essere l'architetto di un'applicazione di grandi dimensioni che deve fornire supporto per l'estendibilità. L'applicazione deve includere un numero potenzialmente grande di componenti più piccoli. Inoltre, la creazione e l'esecuzione di tali componenti deve essere gestita dall'applicazione.

L'approccio più semplice a questo problema è includere i componenti come codice sorgente nell'applicazione e chiamarli direttamente dal codice. Tuttavia, questo approccio comporta alcuni inconvenienti ovvi. In particolare, non è possibile aggiungere nuovi componenti senza modificare il codice sorgente. Benché ammissibile, ad esempio, in un'applicazione Web, questo limite risulta inaccettabile in un'applicazione client. Un altro ostacolo ugualmente problematico è che in alcuni casi non si ha accesso al codice sorgente dei componenti, in quanto sviluppati da terze parti. Inoltre, per lo stesso motivo, non è possibile consentire a terze parti di accedere al codice di propria creazione.

Un approccio leggermente più sofisticato consiste nel fornire un punto o un'interfaccia di estensione, per consentire la separazione tra l'applicazione e i relativi componenti. Se si applica questo modello è possibile fornire un'interfaccia implementabile da un componente e un'API per consentire a quest'ultima di interagire con l'applicazione. Benché elimini l'esigenza di accedere al codice sorgente, questo approccio presenta comunque alcune difficoltà.

Poiché l'applicazione è priva di qualsiasi capacità autonoma di individuare i componenti, è comunque necessario indicarle in modo esplicito quali sono i componenti disponibili da caricare. Questa operazione in genere viene eseguita registrando in modo esplicito i componenti disponibili in un file di configurazione. Ne consegue che garantire la correttezza dei componenti diventa un problema di manutenzione, in particolar modo se la responsabilità dell'esecuzione dell'aggiornamento viene assegnata all'utente finale e non allo sviluppatore.

Inoltre, i componenti sono incapaci di comunicare fra loro, salvo tramite i canali definiti rigidamente appartenenti all'applicazione stessa. Se per una determinata esigenza di comunicazione l'architetto dell'applicazione non ha previsto un canale specifico, di solito tale comunicazione risulta impossibile.

Infine, gli sviluppatori dei componenti devono accettare una dipendenza rigida in merito all'assembly che contiene l'interfaccia che implementano. Ciò ostacola l'utilizzo di un componente in più applicazioni e può inoltre far emergere dei problemi quando si crea un framework di test per i componenti.

Anziché ricorrere alla registrazione esplicita dei componenti disponibili, MEF offre una funzionalità che consente l'individuazione implicita dei componenti, tramite la composizione. Un componente MEF, detto parte, specifica in modo dichiarativo sia le proprie dipendenze (dette importazioni) sia le funzionalità che rende disponibili (dette esportazioni). Quando si crea una parte, il motore di composizione di MEF ne soddisfa le importazioni con le risorse disponibili nelle altre parti.

Questo approccio risolve i problemi discussi nella sezione precedente. Poiché le parti MEF specificano in modo dichiarativo le proprie funzionalità, sono individuabili al runtime. Ne consegue che un'applicazione può usare le parti senza dover ricorrere a riferimenti hardcoded oppure a file di configurazione fragili. MEF consente alle applicazioni di individuare ed esaminare le parti mediante i relativi metadati, senza creare un'istanza o caricare gli assembly di tali parti. Di conseguenza, non c'è alcun bisogno di specificare attentamente come e quando caricare le estensioni.

Oltre alle proprie esportazioni fornite, una parte può specificare le proprie importazioni che verranno compilate dalle altre parti. Questo approccio rende non solo possibile ma persino facile la comunicazione fra le parti. Inoltre, consente un buon factoring del codice. Ad esempio, i servizi comuni a molti componenti possono essere organizzati in una parte distinta e quindi essere modificati o sostituiti facilmente.

Poiché non è richiesta alcuna dipendenza rigida da un determinato assembly dell'applicazione, il modello MEF consente il riutilizzo delle estensioni da un'applicazione a un'altra. Ciò semplifica inoltre lo sviluppo di un test harness, indipendente dell'applicazione, per testare i componenti dell'estensione.

Un'applicazione estensibile scritta tramite MEF dichiara un'importazione che può essere compilata da componenti di estensione. Inoltre, può dichiarare esportazioni per esporre alle estensioni i servizi dell'applicazione. Ogni componente di estensione dichiara un'esportazione e può anche dichiarare importazioni. In questo modo, i componenti di estensione stessi risultano automaticamente estensibili.

MEF è parte integrante di .NET Framework 4 ed è disponibile in tutti i contesti in cui si usa .NET Framework. È possibile usare MEF nelle applicazioni client se usano Windows Form, WPF o qualsiasi altra tecnologia oppure nelle applicazioni server che usano ASP.NET.

Nelle versioni precedenti di .NET Framework è stato introdotto Managed Add-in Framework (MAF), progettato per consentire alle applicazioni di isolare e gestire le estensioni. Rispetto a MEF, MAF si concentra su un livello leggermente più elevato, in particolare sull'isolamento delle estensioni e sul caricamento e scaricamento degli assembly. MEF si concentra invece su individuazione, estensibilità e portabilità. I due framework interoperano agevolmente e una stessa applicazione può trarre vantaggio da entrambi.

Il modo più semplice per vedere le potenzialità di MEF è compilare una semplice applicazione MEF. In questo esempio viene compilata una calcolatrice molto semplice denominata SimpleCalculator. L'obiettivo di SimpleCalculator è creare un'applicazione console che accetta comandi aritmetici di base, nel formato "5+3" o "6-2", e restituisce le risposte corrette. Tramite MEF sarà possibile aggiungere nuovi operatori senza modificare il codice dell'applicazione.

Per scaricare il codice completo di questo esempio, vedere l'esempio di SimpleCalculator.

System_CAPS_ICON_note.jpg Nota

Più che fornire uno scenario di uso realistico, lo scopo di SimpleCalculator è dimostrare i concetti e la sintassi di MEF. Molte delle applicazioni in grado di sfruttare al meglio la potenza di MEF sono più complesse di SimpleCalculator. Per altri esempi dettagliati, vedere Managed Extensibility Framework in Codeplex.

Per iniziare, in Visual Studio 2010, creare un nuovo progetto di applicazione console denominato SimpleCalculator. Aggiungere un riferimento all'assembly System.ComponentModel.Composition in cui risiede MEF. Aprire Module1.vb o Program.cs e aggiungere istruzioni Imports o using per System.ComponentModel.Composition e System.ComponentModel.Composition.Hosting. Questi due spazi dei nomi contengono i tipi MEF che saranno necessari per sviluppare un'applicazione estensibile. In Visual Basic aggiungere la parola chiave Public alla riga in cui viene dichiarato il modulo Module1.

La base del modello di composizione d MEF è il contenitore di composizione. Questo contenitore contiene tutte le parti disponibili ed esegue la composizione, ovvero l'abbinamento tra importazioni ed esportazioni. Il tipo più comune di contenitore di composizione è CompositionContainer e usato anche per SimpleCalculator.

In Visual Basic, aggiungere una classe pubblica denominata Program in Module1.vb. Aggiungere quindi la riga seguente alla classe Program in Module1.vb o Program.cs:

private CompositionContainer _container;  

Per individuare le parti a cui possono accedere, i contenitori di composizione si avvalgono di un catalogo. Un catalogo è un oggetto che rende disponibili le parti individuate in un'origine. MEF fornisce cataloghi per individuare le parti disponibili in un assembly, una directory o un tipo fornito. Gli sviluppatori di applicazioni possono creare facilmente nuovi cataloghi per individuare parti disponibili in altre origini, ad esempio un servizio Web.

Aggiungere il costruttore seguente alla classe Program:

private Program() { //An aggregate catalog that combines multiple catalogs var catalog = new AggregateCatalog(); //Adds all the parts found in the same assembly as the Program class catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly)); //Create the CompositionContainer with the parts in the catalog _container = new CompositionContainer(catalog); //Fill the imports of this object try { this._container.ComposeParts(this); } catch (CompositionException compositionException) { Console.WriteLine(compositionException.ToString()); } }  

La chiamata a ComposeParts indica al contenitore di composizione di comporre un set specifico di parti, in questo caso l'istanza corrente di Program. Tuttavia, a questo punto non si verificherà nulla, poiché Program non presenta importazioni da compilare.

Prima di tutto, Program deve importare una calcolatrice. Ciò consente di separare le problematiche dell'interfaccia utente, ad esempio l'input e l'output della console che andrà in Program, dalla logica della calcolatrice.

Aggiungere il codice seguente alla classe Program:

[Import(typeof(ICalculator))] public ICalculator calculator;  

La dichiarazione dell'oggetto calculator non è insolita, ma è associata all'attributo ImportAttribute. Questo attributo dichiara un elemento di importazione, ovvero un elemento che verrà fornito dal motore di composizione quando l'oggetto viene composto.

Ogni importazione presenta un contratto che determina le esportazioni a cui verrà abbinata. Il contratto può essere una stringa specificata in modo esplicito oppure può essere generato automaticamente da MEF a partire da un tipo specificato, in questo caso l'interfaccia ICalculator. Qualsiasi esportazione dichiarata con un contratto corrispondente verrà usata per soddisfare questa importazione. Benché il tipo dell'oggetto calculator sia effettivamente ICalculator, non si tratta di una condizione obbligatoria. Il contratto è indipendente dal tipo dell'oggetto di importazione. In questo caso, è possibile escludere l'oggetto typeof(ICalculator). Se non specificato in modo esplicito, MEF presuppone automaticamente che il contratto si basi sul tipo di importazione.

Aggiungere questa interfaccia molto semplice al modulo o allo spazio dei nomi SimpleCalculator:

public interface ICalculator { String Calculate(String input); }  

Ora che è stato definito ICalculator, è necessaria una classe che lo implementi. Aggiungere la classe seguente al modulo o allo spazio dei nomi SimpleCalculator:

[Export(typeof(ICalculator))] class MySimpleCalculator : ICalculator { }  

Questa è l'esportazione che verrà abbinata all'importazione in Program. Affinché l'esportazione corrisponda all'importazione, è necessario che dispongano dello stesso contratto. L'esportazione nel contesto di un contratto basato su typeof(MySimpleCalculator) darebbe luogo a una mancata corrispondenza e l'importazione non verrebbe compilata. Il contratto deve corrispondere in modo esatto.

Poiché il contenitore di composizione sarà popolato con tutte le parti disponibili in questo assembly, la parte MySimpleCalculator sarà disponibile. Quando il costruttore di Program esegue la composizione nell'oggetto Program, la relativa importazione verrà compilata con un oggetto MySimpleCalculator che verrà creato a tale scopo.

Al livello dell'interfaccia utente (Program) non occorrono altre informazioni. È quindi possibile compilare il resto della logica dell'interfaccia utente nel metodo Main.

Aggiungere al metodo Main il codice seguente:

static void Main(string[] args) { Program p = new Program(); //Composition is performed in the constructor String s; Console.WriteLine("Enter Command:"); while (true) { s = Console.ReadLine(); Console.WriteLine(p.calculator.Calculate(s)); } }  

Questo codice legge semplicemente una riga di input e chiama la funzione Calculate di ICalculator nel risultato, che quindi scrive nella console. Questo è tutto il codice necessario in Program. Tutte le altre operazioni avranno luogo nelle parti.

Per essere estensibile, SimpleCalculator deve importare un elenco di operazioni. Un attributo ImportAttribute ordinario viene compilato da un solo ExportAttribute. Se ne è disponibile più di uno, il motore di composizione genera un errore. Per creare un'importazione che può essere compilata da qualsiasi numero di esportazioni, è possibile usare l'attributo ImportManyAttribute.

Aggiungere la proprietà operations seguente alla classe MySimpleCalculator:

[ImportMany] IEnumerable<Lazy<IOperation, IOperationData>> operations;  

Lazy<T, TMetadata> è un tipo fornito da MEF per contenere riferimenti indiretti alle esportazioni. Qui, oltre all'oggetto esportato stesso, si ottengono anche metadati di esportazione ovvero informazioni che descrivono l'oggetto esportato. Ciascun Lazy<T, TMetadata> contiene un oggetto IOperation che rappresenta un'operazione effettiva e un oggetto IOperationData che rappresenta i relativi metadati.

Aggiungere le interfacce semplici seguenti al modulo o allo spazio dei nomi SimpleCalculator:

public interface IOperation { int Operate(int left, int right); } public interface IOperationData { Char Symbol { get; } }  

In questo caso, i metadati di ogni operazione sono il simbolo che rappresenta l'operazione, ad esempio +, -, * e così via. Per rendere disponibile l'operazione di addizione, aggiungere la classe seguente al modulo o allo spazio dei nomi SimpleCalculator:

[Export(typeof(IOperation))] [ExportMetadata("Symbol", '+')] class Add: IOperation { public int Operate(int left, int right) { return left + right; } }  

Il funzionamento dell'attributo ExportAttribute non è stato modificato. L'attributo ExportMetadataAttribute allega metadati, nel formato di una coppia nome-valore, a tale esportazione. Mentre la classe Add implementa IOperation, una classe che implementa IOperationData non viene definita in modo esplicito. Tale classe viene invece creata implicitamente da MEF con proprietà basate sui nomi dei metadati forniti. Questo è uno dei vari modi per accedere ai metadati in MEF.

La composizione in MEF è ricorsiva. L'oggetto Program è stato composto in modo esplicito e ha importato un oggetto ICalculator che è risultato essere di tipo MySimpleCalculator.MySimpleCalculator, a sua volta, importa un insieme di oggetti IOperation e tale importazione viene compilata quando viene creato MySimpleCalculator, contemporaneamente alle importazioni di Program. Se la classe Add avesse dichiarato un'altra importazione, sarebbe stato necessario compilare anch'essa e così via. Qualsiasi importazione non compilata comporta un errore di composizione. Tuttavia, è possibile dichiarare le importazioni come facoltative oppure assegnare loro valori predefiniti.

Dopo aver creato queste parti, l'unica operazione che resta da eseguire è creare la logica della calcolatrice stessa. Aggiungere il codice seguente nella classe MySimpleCalculator per implementare il metodo Calculate:

public String Calculate(String input) { int left; int right; Char operation; int fn = FindFirstNonDigit(input); //finds the operator if (fn < 0) return "Could not parse command."; try { //separate out the operands left = int.Parse(input.Substring(0, fn)); right = int.Parse(input.Substring(fn + 1)); } catch { return "Could not parse command."; } operation = input[fn]; foreach (Lazy<IOperation, IOperationData> i in operations) { if (i.Metadata.Symbol.Equals(operation)) return i.Value.Operate(left, right).ToString(); } return "Operation Not Found!"; }  

I passaggi iniziali analizzano la stringa di input in operandi sinistro e destro e carattere dell'operatore. Nel ciclo foreach, viene esaminato ogni membro della raccolta operations. Questi oggetti sono di tipo Lazy<T, TMetadata> e l'accesso ai relativi valori di metadati e al relativo oggetto esportato può essere eseguito rispettivamente con la proprietà Metadata e la proprietà Value. In questo caso, se si verifica la corrispondenza della proprietà Symbol dell'oggetto IOperationData, la calcolatrice chiama il metodo Operate dell'oggetto IOperation e restituisce il risultato.

Per completare la calcolatrice è inoltre necessario il metodo helper che restituisce la posizione del primo carattere non numerico in una stringa. Aggiungere il seguente metodo helper alla classe MySimpleCalculator:

private int FindFirstNonDigit(String s) { for (int i = 0; i < s.Length; i++) { if (!(Char.IsDigit(s[i]))) return i; } return -1; }  

A questo punto è possibile compilare ed eseguire il progetto. In Visual Basic, accertarsi di aver aggiunto la parola chiave Public a Module1. Digitare un'operazione di addizione nella finestra della console, ad esempio "5+3". La calcolatrice restituirà i risultati. Se si usano altri operatori, viene visualizzato un messaggio che indica che l'operazione non è stata trovata .

Ora che la calcolatrice funziona risulta facile aggiungere una nuova operazione. Aggiungere la classe seguente al modulo o allo spazio dei nomi SimpleCalculator:

[Export(typeof(IOperation))] [ExportMetadata("Symbol", '-')] class Subtract : IOperation { public int Operate(int left, int right) { return left - right; } }  

Compilare ed eseguire il progetto. Digitare un'operazione di sottrazione, ad esempio "5-3". Oltre all'addizione, la calcolatrice supporta ora anche la sottrazione.

Benché aggiungere classi al codice sorgente sia abbastanza semplice, MEF offre la possibilità di cercare le parti all'esterno del codice sorgente di un'applicazione. Per dimostrarlo sarà necessario modificare SimpleCalculator affinché cerchi le parti in una directory nonché nel proprio assembly mediante l'aggiunta di DirectoryCatalog.

Aggiungere al progetto SimpleCalculator una nuova directory denominata Extensions. Assicurarsi di aggiungerla a livello di progetto e non a livello di soluzione. Aggiungere quindi alla soluzione un nuovo progetto Libreria di classi denominato ExtendedOperations. Il nuovo progetto verrà compilato in un assembly separato.

Aprire la finestra di progettazione Proprietà progetto per il progetto ExtendedOperations e fare clic sulla schedaCompila. Modificare Percorso dell'output di compilazione o Percorso output affinché punti alla directory Extensions nella directory del progetto SimpleCalculator (..\SimpleCalculator\Extensions\).

In Module1.vb o Program.cs aggiungere la riga seguente al costruttore Program:

catalog.Catalogs.Add(new DirectoryCatalog("C:\\SimpleCalculator\\SimpleCalculator\\Extensions"));  

Sostituire il percorso di esempio con il percorso della directory Extensions. Il percorso assoluto è solo a scopo di debug. In un'applicazione di produzione si usa un percorso relativo. A questo punto, DirectoryCatalog aggiungerà al contenitore di composizione qualsiasi parte trovata negli assembly contenuti nella directory Extensions.

Nel progetto ExtendedOperations aggiungere i riferimenti a SimpleCalculator e System.ComponentModel.Composition. Nel file di classe ExtendedOperations aggiungere un'istruzione Imports o using per System.ComponentModel.Composition. In Visual Basic aggiungere anche un'istruzione Imports per SimpleCalculator. Quindi, aggiungere la seguente classe al file di classe ExtendedOperations:

[Export(typeof(SimpleCalculator.IOperation))] [ExportMetadata("Symbol", '%')] public class Mod : SimpleCalculator.IOperation { public int Operate(int left, int right) { return left % right; } }  

Per la corrispondenza del contratto, l'attributo ExportAttribute deve avere lo stesso tipo di ImportAttribute.

Compilare ed eseguire il progetto. Testare il nuovo operatore Mod (%).

In questo argomento sono stati trattati i concetti di base di MEF.

  • Parti, cataloghi e contenitore di composizione

    Le parti e il contenitore di composizione sono i blocchi predefiniti di base di un'applicazione MEF. Una parte è qualsiasi oggetto che importa o esporta un valore, fino al proprio valore (incluso). Un catalogo fornisce un insieme di parti ottenute da una determinata origine. Il contenitore di composizione usa le parti fornite da un catalogo per eseguire la composizione, ovvero il binding tra le importazioni e le esportazioni.

  • Importazioni ed esportazioni

    Le importazioni e le esportazioni rappresentano il canale di comunicazione fra i componenti. Con un'importazione il componente specifica la necessità di un determinato valore o oggetto, mentre con un'esportazione specifica la disponibilità di un valore. Ogni importazione viene associata a un elenco di esportazioni mediante il relativo contratto.

Per scaricare il codice completo di questo esempio, vedere l'esempio di SimpleCalculator.

Per altre informazioni e per esempi di codice, vedere Managed Extensibility Framework. Per un elenco di tipi MEF, vedere lo spazio dei nomi System.ComponentModel.Composition.

Mostra: