Megjegyzés: Managed Extensibility Framework .NET Core óta nem része a keretrendszernek alapértelmezetten. .NET Core és újabb projektek esetén a System.ComponentModel.Composition NuGet csomag telepítésével használható.
A .NET keretrendszer 4.0 változatában mutatkozott be a Managed Extensibility Framework vagy röviden MEF, ami kifejezetten a moduláris alkalmazások fejlesztését könnyíti meg. A programunk tervezésekor felmerülhet igényként, hogy beépülő modulokkal (Plugin) bővíthető legyen a tudása.
Ebben az esetben feltalálhatjuk a spanyol viaszt újra és reflection segítségével tölthetünk be modulokat, vagy használhatjuk a MEF által biztosított keretrendszert, amivel drámaian leegyszerűsíthetjük a feladatot.
A MEF egy dependency injection-t valósít meg. Ez azonban nem jelenti azt, hogy alkalmas lenne egy univerzális IoC konténer megvalósítására. Ennek az oka leginkább abban keresendő, hogy amíg a MEF mindig példányosít, típusokat hoz létre és példányokat tárol, addig egy általános IoC konténer csak akkor példányosít egy típust, amikor szükséges.
A MEF használatát is egy példán keresztül a legegyszerűbb elsajátítani. A dependency injection tárgyalásakor készítettünk egy egyszerű shell kezdeményt, ami moduláris felépítésű volt. Itt ezt a példát írjuk át MEF kompatibilis módon.
Egy teljesen moduláris alkalmazás számos dll fájlból áll. Ezekből néhányat akár maga a felhasználó is szállíthat. Éppen ezért először definiálnunk kell a sablon interfészeket, amik a modulok működéséhez kellenek. Ezeket a sablon interfészeket külön szerelvényben érdemes definiálni, így a futtató alkalmazás implementációja és a modulok is a köztes interfész definíciókra támaszkodhatnak.
Ennek előnye, hogy ha külön modult fejlesztünk, akkor elegendő a modulhoz tartozó projectben referenciaként hivatkozni az interfészek definícióját tartalmazó szerelvényre. A példaprogramunkban két interfészt hozunk létre. Az IModule fogja leírni a parancsok végrehajtásáért felelős modult, az IHost felület pedig a modul futtatókörnyezetét írja le.
Az IModule definíciója:
namespace MEF.API
{
public interface IModule
{
IHost Host { get; set; }
string Name { get; }
void Run();
}
}
Az IHost definíciója:
namespace MEF.API
{
public interface IHost
{
void WriteLine(string format, params object[] parameters);
void Write(string format, params object[] parameters);
void Clear();
string WorkDirectory { get; }
}
}
A Host jelen esetben csak egy proxy a konzol felé, de akár fájlba írást vagy terminálkezelést is megvalósíthatna. A Host implementációja a fő programban kapott helyet:
using MEF.API;
using System;
using System.ComponentModel.Composition;
namespace MEF
{
[Export(typeof(IHost))]
public class Host : IHost
{
public string WorkDirectory
{
get { return Environment.CurrentDirectory; }
}
public void Clear()
{
Console.Clear();
}
public void Write(string format, params object[] parameters)
{
Console.Write(format, parameters);
}
public void WriteLine(string format, params object[] parameters)
{
Console.WriteLine(format, parameters);
}
}
}
A Host osztály előtt feltüntetett Export attribútum jelzi a MEF-nek, hogy az adott típus egy példánya a typeof által meghatározott interfésznek. Ennek használatához szükséges a System.ComponentModel.Composition névtér, ami a System.ComponentModel.Composition szerelvényben kapott helyet.
A betöltést és a futtatást megvalósító kód a Runner osztályba került. A MEF alaposztálya a CompositionContainer, ami azon osztályok példányait tárolja, amelyek meg vannak jelölve az Export attribútummal. A konténernek meg kell adni egy katalógust, amiben a típusokat keresi. A katalógus jelenthet itt egy konkrét szerelvényt, vagy egy mappát amiben szerelvények találhatóak. A futtatókörnyezet mind a két megoldást használja, mert az IHost implementációja a főprogram szerelvényében található. Az egyes modulok pedig külön dll fájlokban lehetnek.
using MEF.API;
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Reflection;
namespace MEF
{
public class Runner
{
//tároló a moduloknak
//Az ImportMany jelzi a MEF-nek, hogy egy kolleciót kell feltöltenie
[ImportMany(typeof(IModule))]
private IEnumerable<IModule> _modules;
private IHost _host;
public void DoLoad()
{
//Katalógusokat tároló katalógus
var catalog = new AggregateCatalog();
//Az Export atribútummal megjelölt típusok keresése az aktuális szerelvényben
catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
//Szerelvények betöltése az adott mappából és az Export atribútummal megjelölt típusok betöltése
catalog.Catalogs.Add(new DirectoryCatalog(Environment.CurrentDirectory));
//Kompozíciós konténer létrehozása. A betöltés itt valósul meg.
CompositionContainer container = new CompositionContainer(catalog);
//IHost elérhetőségének tesztelése.
_host = container.GetExportedValue<IHost>();
if (_host == null)
{
Console.WriteLine("Nincs host. Valami gond van");
Environment.Exit(-1);
}
//A modulok betöltése
container.ComposeParts(this);
//A modulok számára az IHost függőség kielégítése
container.SatisfyImportsOnce(this);
}
//A megadott névvel rendelkező példány keresése, majd a Run metódusának meghívása
public void Run()
{
do
{
Console.Write("Parancs: ");
var cmd = Console.ReadLine();
var selected = _modules.FirstOrDefault(i => i.Name == cmd);
if (selected != null)
{
selected.Run();
}
else if (cmd == "Exit")
{
break;
}
else
{
_host.WriteLine("Nem található: {0}", cmd);
}
}
while (true);
}
}
}
A modulok a _modules változóba kerülnek betöltésre a container.ComposeParts(this); hívásával. A MEF-nek az ImportMany attribútum mondja meg, hogy a _modules változó a tárolója az IModule példányoknak. A container.SatisfyImportsOnce(this); hívás végzi el a létrejött példányokon a függőségek injektálását. A modult futtató Run metódus ugyanaz, mint amit egy korábbi példában használtunk.
A modulok implementációjában is annotálni kell a típusokat. Az Export attribútummal publikáljuk a modult, míg az Import attribútum a függőségek jelzésére szolgál.
using MEF.API;
using System.ComponentModel.Composition;
namespace MEF.Modulok
{
[Export(typeof(IModule))]
public class ModuleHello : IModule
{
[Import(typeof(IHost))]
public IHost Host { get; set; }
public string Name => "Hello";
public void Run()
{
Host?.WriteLine("Hello");
}
}
[Export(typeof(IModule))]
public class ModuleCls : IModule
{
[Import(typeof(IHost))]
public IHost Host { get; set; }
public string Name => "Clear";
public void Run()
{
Host?.Clear();
}
}
[Export(typeof(IModule))]
public class ModulePwd: IModule
{
[Import(typeof(IHost))]
public IHost Host { get; set; }
public string Name => "Pwd";
public void Run()
{
Host?.WriteLine(Host?.WorkDirectory);
}
}
}
A tesztprogram kódja a következő:
using System;
namespace MEF
{
class Program
{
static void Main(string[] args)
{
Runner runner = new Runner();
runner.DoLoad();
runner.Run();
Console.ReadKey();
}
}
}
A program egy lehetséges kimenete:
Command: ASD
Nem található: ASD
Command: Hello
Hello
Command: Exit