Managed Extensibility Framework 或 MEF 是一个用于创建可扩展的轻型应用程序的库。 应用程序开发人员可利用该库发现并使用扩展,而无需进行配置。 扩展开发人员还可以利用该库轻松地封装代码,避免生成脆弱的硬依赖项。 通过 MEF,不仅可以在应用程序内重用扩展,还可以在应用程序之间重用扩展。
假设您是一个必须提供扩展性支持的大型应用程序的架构师。 您的应用程序必须包含大量可能需要的较小组件,并负责创建和运行这些组件。
解决这一问题的最简单的方法是:将这些组件作为源代码包括在您的应用程序中,然后通过代码直接调用它们。 这种做法存在很多明显的缺陷。 最重要的是,您无法在不修改源代码的情况下添加新组件,这一限制在 Web 应用程序(举例来说)中也许能够接受,但在客户端应用程序中行不通。 同样存在问题的还有,您可能没有对组件的源代码的访问权,因为这些组件可能是由第三方开发的,而出于相同的原因,您也不允许第三方访问您的代码。
一种稍微复杂的方法是:提供扩展点或接口,以允许应用程序与其组件相分离。 依据此模型,您可能会提供一个组件能够实现的接口,并提供一个 API 以使该接口能够与您的应用程序进行交互。 这一方法可解决需要源代码访问权的问题,但仍具有自己的难点。
由于应用程序缺乏自己发现组件的能力,因此仍必须明确告知应用程序哪些组件可用并应加载。 这通常是通过在一个配置文件中显式注册可用组件来实现的。 这意味着,确保组件正确无误成为了一个日常维护问题,尤其是在执行更新操作的是最终用户而非开发人员的情况下。
此外,各组件之间无法进行通信,除非是通过应用程序自身的严格定义的通道。 如果应用程序架构师未预计到需要某项通信,则通常是无法进行相应的通信的。
最后,组件开发人员不得不硬依赖于包含他们实现的接口的程序集。 这样就很难在多个应用程序中使用同一个组件,另外,在为组件创建测试框架时也会造成问题。
有别于这种显式注册可用组件的做法,MEF 提供一种通过“组合”隐式发现组件的方法。 MEF 组件(称为“部件”)以声明方式同时指定其依赖项(称为“导入”)及其提供的功能(称为“导出”)。 创建一个部件时,MEF 组合引擎会使其导入与其他部件提供的内容相符合。
此方法解决了上一节中讨论的问题。 由于 MEF 部件以声明方式指定其功能,因此在运行时可发现这些部件。这意味着,应用程序无需硬编码的引用或脆弱的配置文件即可利用相关部件。 通过 MEF,应用程序可以通过部件的元数据来发现并检查部件,而不用实例化部件,或者甚至不用加载部件的程序集。 因此,没有必要仔细指定应何时以及如何加载扩展。
除了部件提供的导出以外,部件还可以指定其导入,然后由其他部件填充这些导入。 这不仅使各部件之间的通信变为可能,而且使通信变得很容易,此外,还可以合理地分解代码。 例如,可以将许多组件的公用服务分解到单独的部件中,以便于修改或替换。
由于 MEF 模型不要求硬依赖于特定的应用程序程序集,因此,此模型允许在应用程序之间重用扩展。 利用此模型,还可以轻松地开发独立于应用程序的测试工具来测试扩展组件。
使用 MEF 编写的可扩展应用程序会声明一个可由扩展组件填充的导入,而且还可能会声明导出,以便向扩展公开应用程序服务。 每个扩展组件都会声明一个导出,而且还可能会声明导入。 通过这种方式,扩展组件本身是自动可扩展的。
MEF 是 .NET Framework 4 的组成部分,可用在任何使用 .NET Framework 的地方。 可以在客户端应用程序中使用 MEF(无论应用程序使用的是 Windows 窗体、WPF,还是任何其他技术),也可以在使用 ASP.NET 的服务器应用程序中使用 MEF。
技术介绍:
示例:
以实例说话,一起体验MEF带来的可扩展性吧,Let’s Rock!!!
1:新建控制台程序SimpleCalculator
在这里要实现的程序时SimpleCalculator,顾名思义:简单的计算器。
所以我们需要定义一个用来计算的接口:
public interface ICalculator
{
String Calculate(String input);
}
Program 的代码如下:
class Program
{
private CompositionContainer _container;
[Import(typeof(ICalculator))]
private ICalculator calculator;
public Program()
{
//var catalog = new AggregateCatalog();
//catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
var catalog = new AssemblyCatalog(typeof(Program).Assembly);
_container = new CompositionContainer(catalog);
try
{
this._container.ComposeParts(this);
}
catch (CompositionException compositionException)
{
Console.WriteLine(compositionException.ToString());
}
}
static void Main(string[] args)
{
Program p = new Program();
string s;
Console.WriteLine("Enter Command:");
while (true)
{
s = Console.ReadLine();
Console.WriteLine(p.calculator.Calculate(s));
}
}
}
MEF所要解决的是寻找插件的功能,传统的实现插件的方式主要是使用接口,即声明一个接口,然后使用配置文件来配置接口使用哪个实现类。
微软知道有这种需求,于是提供了MEF来实现插件的功能。
Composite 原理:
1:声明一个 CompositionContainer 对象,这个对象里面包含一堆Catalog.
2:这堆Catalog如果是AssemblyCatalog,则在Assembly中查找,如果是DirectoryCatalog,
在Directory 中查找,如果即想要在Assembly中查找,又需要在Directory中查找,
则采用AggregateCatalog。
3:然后在这堆Catalog中查找与Import 特性相对应的Export标记所标记的实现类,调用实现类的构造函数进行
Composite(组合)。
知道原理后,你也可以自己实现自己的CompositionContainer 类了,
要使用MEF 需要为SimpleCalculator添加 System.ComponentModel.Composition.dll 的引用,
然后导入命名空间:
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
接下来看下Program 的构造函数所做的事情:
声明一个AssemblyCatalog,指向Program所在的Assembly. 然后把它添加到
CompositionContainer中,调用CompositionContainer 的ComposeParts 扩展方法,来Compose(this) 的Parts。
注:ComposeParts 是扩展方法,需要using System.ComponentModel.Composition;
OK,如何Compose,在哪个Assembly中查找实现类来进行Compose已经完成了。
目前的问题是:哪些类需要Compose??
为了回答这个问题,微软提供了Import和Export特性:
Import:哪个对象需要Compose。也就是需要被实现类给填充,所以Import标记的是对象,一般该对象是接口,因为如果是具体类的话,那还需要Import吗?
Export:哪个类可以被用来Compose,也就是说这个类是不是可以用来填充的实现类,所以Export标记的是类,而不是具体的某个对象。
所以在这里calculator 使用Import 特性来标记:
[Import(typeof(ICalculator))]
private ICalculator calculator;
接下来MEF 的组合引擎在ComposeParts(this)的时候,就会在catalog 代表的AssemblyCatalog中查找Export特性所修饰的实现类了,找到实现类后进行Compose。
如果找不到Export特性修饰的类的话,结果如下:
OK,接下来添加一个实现类,并使用Export特性来进行修饰:
[Export(typeof(ICalculator))]
public class MySimpleCalculator : ICalculator
{
public string Calculate(string input)
{
return "MySimpleCalculator 处理了" + input;
}
}
运行结果如下:
当然Import和Export还提供了其他的构造函数,所以你还可以将上面的Import和Export修改为:
[Import("calculator1", typeof(ICalculator))]
[Export("calculator1", typeof(ICalculator))]
之所以提供ContractName为calculator1 是因为你可能有多个ICalculator对象需要填充。
修改Program的代码如下:
class Program
{
private CompositionContainer _container;
[Import("calculator1", typeof(ICalculator))]
private ICalculator calculator1;
[Import("calculator2", typeof(ICalculator))]
private ICalculator calculator2;
public Program()
{
//var catalog = new AggregateCatalog();
//catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
var catalog = new AssemblyCatalog(typeof(Program).Assembly);
_container = new CompositionContainer(catalog);
try
{
this._container.ComposeParts(this);
}
catch(CompositionException compositionException)
{
Console.WriteLine(compositionException.ToString());
}
}
static void Main(string[] args)
{
Program p = new Program();
string s;
Console.WriteLine("Enter Command:");
while (true)
{
s = Console.ReadLine();
Console.WriteLine(p.calculator1.Calculate(s));
Console.WriteLine(p.calculator2.Calculate(s));
}
}
}
修改Export修饰的类为:
[Export("calculator1", typeof(ICalculator))]
public class MySimpleCalculator1 : ICalculator
{
public string Calculate(string input)
{
return "第一个Calculator 处理了" + input;
}
}
[Export("calculator2", typeof(ICalculator))]
public class MySimpleCalculator2 : ICalculator
{
public string Calculate(string input)
{
return "第二个Calculator 处理了" + input;
}
}
运行结果如下:
因为Import和Export是一一对应的,在现实世界中,存在着大量一对多的情况,微软也预料到了这种情况,所以提供了ImportMany 特性。
在上个例子中的MySimpleCalculator的Calculate方法返回的是一句话,在这个例子中要真正实现计算的功能,例如输入5+3,输出8,输入7*4,输出28。
为了支持 + - * / 四种Operation.所以在MySimpleCalculator中声明一个operations 的列表。
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;
public string Calculate(string input)
{
return "calculate 处理了" + input;
}
}
之所以在MySimpleCalculator 中声明operations ,是因为是计算器支持多种运算。因为operations 需要多个operation 来Compose(填充),所以使用ImportMany特性来修饰,和Import特性一样,ImportMany特性一般也是修饰接口。
Ioperation 和IOperationData的定义如下:
public interface IOperation
{
int Operate(int left, int right);
}
public interface IOperationData
{
Char Symbol { get; }
}
Lazy<IOperation, IOperationData> operations:提供对对象及其关联的元数据的延迟间接引用,以供 Managed Extensibility Framework 使用。
意思是说IOperation 和IOperationData之间的引用需要延迟,为什么需要延迟?,因为IOperation需要根据IOperationData的Symbol符号来延迟创建。
也就是说,如果IOperationData的Symbol 等于 “+”,那么IOperation对象是AddOperation.如果IOperationData的Symbol等于”-”,那么IOperation对象是SubtractOperation.
那么如何保证这点呢?
关键点就在于ExportMetadata attribute 上。
看下Add Operation 的定义:
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add : IOperation
{
public int Operate(int left, int right)
{
return left + right;
}
}
在这里ExportMetadata特性的Symbol 为+。所以当IOperationData的Symbol为”+” 的时候,匹配的就是Add Operation
MySimpleCalculator 的完整代码如下:
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;
public string Calculate(string input)
{
int left;
int right;
char operation;
int fn = FindFirstNonDigitPosition(input);
if (fn < 0) return "Could not parse command.";
try
{
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!";
}
private int FindFirstNonDigitPosition(string s)
{
for (int i = 0; i < s.Length; i++)
{
if (!(Char.IsDigit(s[i]))) return i;
}
return -1;
}
}
回头再看看上例的Program代码:
class Program
{
private CompositionContainer _container;
[Import(typeof(ICalculator))]
private ICalculator calculator;
public Program()
{
//var catalog = new AggregateCatalog();
//catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
var catalog = new AssemblyCatalog(typeof(Program).Assembly);
_container = new CompositionContainer(catalog);
try
{
this._container.ComposeParts(this);
}
catch(CompositionException compositionException)
{
Console.WriteLine(compositionException.ToString());
}
}
static void Main(string[] args)
{
Program p = new Program();
string s;
Console.WriteLine("Enter Command:");
while (true)
{
s = Console.ReadLine();
Console.WriteLine(p.calculator.Calculate(s));
}
}
}
当this._container.ComposeParts(this); 的时候,MEF组合引擎就开始对标记了Import特性的接口进行Compose,所以在这里是calculator。在哪里找实现类呢?,AssemblyCatalog表明在Program的当前Assembly中查找实现类,所以找到了MySimpleCalculator在构造MySimpleCalculator 的时候,发现了ImportMany特性修饰的operations。于是继续在AssemblyCatalog中找到了Add。
上面的过程是Compose的过程。
那么MySimpleCalculator 如何进行Calculate的呢?
例如5+3
1:找出第一个非数字的位置,也就是需要找出 +。
2:声明left,right.并且left 为5,right为3.
3:根据符号+来构造IOperation对象,接着调用IOperation对象的Operate(left,right)方法。
foreach (Lazy<IOperation, IOperationData> i in operations)
{
if (i.Metadata.Symbol.Equals(operation))
return i.Value.Operate(left, right).ToString();
}
运行结果:
因为目前定义了Add 的Operation。所以根据符号+ 能够找到Add,但是*我们没有定义,所以Operation Not Found!.
于是开始定义Multiple:
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '*')]
class Multiple : IOperation
{
public int Operate(int left, int right)
{
return left * right;
}
}
再次运行,结果如下:
当然还可以在当前程序集下面增加- ,/,^,% 等Operation。
为了让事情更加的有趣,我打算在Debug目录下增加一个目录CalculateExtensions,然后将-,/ ..的Operation放到里面来,让MEF自动发现。
首先新建类库项目:SimpleCalculatorExtension
因为需要实现IOperation ,所以需要添加对SimpleCalculator项目的引用。
因为需要Export特性,所以需要添加对System.ComponentModel.Composition的引用。
整个项目的结果如下:
Subtract代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
namespace SimpleCalculatorExtension
{
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : SimpleCalculator.IOperation
{
public int Operate(int left, int right)
{
return left - right;
}
}
}
生成成功后,将SimpleCalculatorExtension.dll 拷贝到CalculateExtensions目录下:
现在SimpleCalculator的Debug目录应该是这样。
并且CalculateExtensions文件夹下面有SimpleCalculatorExtension.dll.
接下来唯一要修改的是Program的catalog 对象。
为了让catalog既支持在Program的Assembly中查找,又支持在CalculateExtensions目录下查找。修改代码如下:
public Program()
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
catalog.Catalogs.Add(new DirectoryCatalog("CalculateExtensions"));
_container = new CompositionContainer(catalog);
try
{
this._container.ComposeParts(this);
}
catch(CompositionException compositionException)
{
Console.WriteLine(compositionException.ToString());
}
}
运行结果如下:
修改SimpleCalculatorExtension 的Subtract方法为:
namespace SimpleCalculatorExtension
{
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : SimpleCalculator.IOperation
{
public int Operate(int left, int right)
{
Console.WriteLine("SimpleCalculatorExtension的方法");
return left - right;
}
}
}
重新生成SimpleCalculatorExtension.dll 然后拷贝到CalculateExtensions 文件夹下:
再次运行程序,输出入下: