Il Managed Extensibility Framework è un potente framework che permette di estendere in modo semplice le nostre applicazioni. È parte integrante del Framework 4.0 ed è disponibile al download per la versione 3.5 a questo link dove si trovano anche esempi molto interessanti, documentazione e discussioni proficue.
Lo scenario che ho voluto indirizzare è di un servizio noto che vuole utilizzare dei plugin esterni. Tanto per capirci non è uno scenario (peraltro possibile) nel quale gli addin sono i servizi (implementazione del contrratto WCF) da esporre.
Uno dei cardini di MEF è il catalog che si occupa di mantenere le informazioni degli assembly delle estensioni (le dll non referenziate all'applicazione principale che verranno caricate ed eseguite da MEF a runtime). Per questo motivo il catalog tipicamente ha lo stesso ciclo di vita della nostra applicazione.
Mentre nelle applicazioni web il ciclo di vita può essere mantentenuto in HttpApplication nel global.asax o in un modulo, per le applicazioni WCF le cose sono un po' diverse, soprattutto perché potremmo decidere di hostare il servizio WCF anche senza IIS.
La soluzione che ho adottato nel progetto su cui sto lavorando è quella di creare un semplice Instance Provider di WCF che mantiene in vita il catalog di MEF ed esegue le operazioni basilari (popolare gli imports del servizio). Il modo più 'pulito' per realizzare un instance provider è quello di realizzare un behavior sotto forma di attributo.
Il risultato finale sarà perciò solo di decorare il nostro servizio con un nostro attributo chiamato MEFBehavior:
[MEFBehavior]
public class MyService : IMyService
{ ... }
La realizzazione del behavior è molto semplice e si limita all'implementazione di un paio di interfacce. Il behavior ha sostanzialmente solo lo scopo di creare l'instance provider.
public class MEFBehaviorAttribute : Attribute, IContractBehavior, IContractBehaviorAttribute
{
public Type TargetContract
{
get { return null; }
}
public void AddBindingParameters(ContractDescription description, ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection parameters)
{
}
public void ApplyClientBehavior(ContractDescription description, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}
public void ApplyDispatchBehavior(ContractDescription description, ServiceEndpoint endpoint, DispatchRuntime dispatch)
{
dispatch.InstanceProvider = new MEFInstanceProvider();
}
public void Validate(ContractDescription description, ServiceEndpoint endpoint)
{
}
}
La proprietà TargetContract ritorna null in modo da applicarsi a qualsiasi contratto, come specificato qui.
L'instance provider si occupa di creare il catalog una sola volta e poi di soddisfare i requisiti MEF del servizio ad ogni istanza creata, il tutto condito con un po' di logica di sincronizzazione per evitare spiacevoli potenziali inconvenienti da accesso multi-threading che potrebbero verificarsi nonostante il "true" nel secondo parametro del CompositionContainer (che indica di volere la thread safety):
public class MEFInstanceProvider : IInstanceProvider
{
private static CompositionContainer _container;
private static object _synclock;
static MEFInstanceProvider()
{
_synclock = new object();
Compose();
}
public object GetInstance(System.ServiceModel.InstanceContext instanceContext, System.ServiceModel.Channels.Message message)
{
var serviceType = instanceContext.Host.Description.ServiceType;
var instance = Activator.CreateInstance(serviceType);
ComposeInstance(instance);
return instance;
}
public object GetInstance(System.ServiceModel.InstanceContext instanceContext)
{
return GetInstance(instanceContext, null);
}
public void ReleaseInstance(System.ServiceModel.InstanceContext instanceContext, object instance)
{
var disposable = instance as IDisposable;
if (disposable != null)
disposable.Dispose();
}
private void ComposeInstance(object instance)
{
lock (_synclock)
{
CompositionBatch batch = new CompositionBatch();
batch.AddPart(instance);
try
{
_container.Compose(batch);
}
catch (CompositionException compositionException)
{
Trace.WriteLine(compositionException.ToString());
}
catch (Exception err)
{
Trace.WriteLine(err.ToString());
}
}
}
private static void Compose()
{
var catalog = new AggregateCatalog();
var layoutFolder = ConfigurationHelper.GetLayoutFolder().FullName;
catalog.Catalogs.Add(new DirectoryCatalog(layoutFolder));
_container = new CompositionContainer(catalog, true);
}
}
ConfigurationHelper.GetLayoutFolder è un mio helper per recuperare la cartella degli addin dagli appSettings della configurazione.
Tutto il resto è esattamente uguale a ciò che viene fatto nelle applicazioni che usano MEF. Ad esempio il servizio esporrà un membro con la collection di plugins in modo simile a questo:
[ImportMany]
private Lazy<IPrintDataProcessor, IPrintCategory>[] PrintModules { get; set; }
Semplice e potente, non si può proprio dire altro!