Managed Extensibility Framework 概述

更新:2010 年 7 月

本主题提供了 .NET Framework 4 中引入的 Managed Extensibility Framework 的概述。

本主题包括下列各节。

  • 什么是 MEF?
  • 扩展性问题
  • MEF 提供的内容
  • MEF 可用在哪些地方?
  • MEF 和 MAF
  • SimpleCalculator:示例应用程序
  • 组合容器和目录
  • 使用特性的导入和导出
  • 进一步的导入和 ImportMany
  • 计算器逻辑
  • 使用新类扩展 SimpleCalculator
  • 使用新程序集扩展 SimpleCalculator
  • 结束语
  • 现在转到什么位置?

什么是 MEF?

Managed Extensibility Framework 或 MEF 是一个用于创建可扩展的轻型应用程序的库。 应用程序开发人员可利用该库发现并使用扩展,而无需进行配置。 扩展开发人员还可以利用该库轻松地封装代码,避免生成脆弱的硬依赖项。 通过 MEF,不仅可以在应用程序内重用扩展,还可以在应用程序之间重用扩展。

扩展性问题

假设您是一个必须提供扩展性支持的大型应用程序的架构师。 您的应用程序必须包含大量可能需要的较小组件,并负责创建和运行这些组件。

解决这一问题的最简单的方法是:将这些组件作为源代码包括在您的应用程序中,然后通过代码直接调用它们。 这种做法存在很多明显的缺陷。 最重要的是,您无法在不修改源代码的情况下添加新组件,这一限制在 Web 应用程序(举例来说)中也许能够接受,但在客户端应用程序中行不通。 同样存在问题的还有,您可能没有对组件的源代码的访问权,因为这些组件可能是由第三方开发的,而出于相同的原因,您也不允许第三方访问您的代码。

一种稍微复杂的方法是:提供扩展点或接口,以允许应用程序与其组件相分离。 依据此模型,您可能会提供一个组件能够实现的接口,并提供一个 API 以使该接口能够与您的应用程序进行交互。 这一方法可解决需要源代码访问权的问题,但仍具有自己的难点。

由于应用程序缺乏自己发现组件的能力,因此仍必须明确告知应用程序哪些组件可用并应加载。 这通常是通过在一个配置文件中显式注册可用组件来实现的。 这意味着,确保组件正确无误成为了一个日常维护问题,尤其是在执行更新操作的是最终用户而非开发人员的情况下。

此外,各组件之间无法进行通信,除非是通过应用程序自身的严格定义的通道。 如果应用程序架构师未预计到需要某项通信,则通常是无法进行相应的通信的。

最后,组件开发人员不得不硬依赖于包含他们实现的接口的程序集。 这样就很难在多个应用程序中使用同一个组件,另外,在为组件创建测试框架时也会造成问题。

MEF 提供的内容

有别于这种显式注册可用组件的做法,MEF 提供一种通过“组合”隐式发现组件的方法。 MEF 组件(称为“部件”)以声明方式同时指定其依赖项(称为“导入”)及其提供的功能(称为“导出”)。 创建一个部件时,MEF 组合引擎会使其导入与其他部件提供的内容相符合。

此方法解决了上一节中讨论的问题。 由于 MEF 部件以声明方式指定其功能,因此在运行时可发现这些部件。这意味着,应用程序无需硬编码的引用或脆弱的配置文件即可利用相关部件。 通过 MEF,应用程序可以通过部件的元数据来发现并检查部件,而不用实例化部件,或者甚至不用加载部件的程序集。 因此,没有必要仔细指定应何时以及如何加载扩展。

除了部件提供的导出以外,部件还可以指定其导入,然后由其他部件填充这些导入。 这不仅使各部件之间的通信变为可能,而且使通信变得很容易,此外,还可以合理地分解代码。 例如,可以将许多组件的公用服务分解到单独的部件中,以便于修改或替换。

由于 MEF 模型不要求硬依赖于特定的应用程序程序集,因此,此模型允许在应用程序之间重用扩展。 利用此模型,还可以轻松地开发独立于应用程序的测试工具来测试扩展组件。

使用 MEF 编写的可扩展应用程序会声明一个可由扩展组件填充的导入,而且还可能会声明导出,以便向扩展公开应用程序服务。 每个扩展组件都会声明一个导出,而且还可能会声明导入。 通过这种方式,扩展组件本身是自动可扩展的。

MEF 可用在哪些地方?

MEF 是 .NET Framework 4 的组成部分,可用在任何使用 .NET Framework 的地方。 可以在客户端应用程序中使用 MEF(无论应用程序使用的是 Windows 窗体、WPF,还是任何其他技术),也可以在使用 ASP.NET 的服务器应用程序中使用 MEF。

MEF 和 MAF

早期的 .NET Framework 版本引入了 Managed Add-in Framework (MAF),旨在使应用程序能够隔离和管理扩展。 MAF 的重点放在比 MEF 稍高的级别,它集中于扩展隔离以及程序集的加载和卸载,而 MEF 则集中于可发现性、扩展性和可移植性。 这两个框架可以顺利地进行互操作,并且单个应用程序可以同时利用这两个框架。

SimpleCalculator:示例应用程序

查看 MEF 可执行的操作的最简单方法是生成简单的 MEF 应用程序。 在本示例中,将生成名为 SimpleCalculator 的一个非常简单的计算器。 SimpleCalculator 的目标是创建一个控制台应用程序,该应用程序接受“5+3”或“6-2”形式的基本算术命令,并返回正确的答案。 通过使用 MEF,您将能够在不更改应用程序代码的情况下添加新的运算符。

若要下载本示例的完整代码,请参见 SimpleCalculator sample(SimpleCalculator 示例)。

注意注意

SimpleCalculator 的目的是演示 MEF 的概念和语法,而不是一定要提供其用法的真实方案。许多可从 MEF 的强大功能获得最大好处的应用程序都比 SimpleCalculator 更加复杂。有关更多详细示例,请参见 Codeplex 上的 Managed Extensibility Framework

首先,在 Visual Studio 2010 中,创建名为 SimpleCalculator 的新控制台应用程序项目。 添加对 MEF 所在的 System.ComponentModel.Composition 程序集的引用。 打开 Module1.vb 或 Program.cs 并为 System.ComponentModel.Composition 和 System.ComponentModel.Composition.Hosting 添加 Imports 或 using 语句。 这两个命名空间包含您开发可扩展应用程序时将需要的 MEF 类型。 在 Visual Basic 中,向声明 Module1 模块的行添加 Public 关键字。

组合容器和目录

MEF 组合模型的核心是组合容器,该容器包含所有可用的部件并执行组合操作 (即,将导入和导出配对)。组合容器的最常见类型是 CompositionContainer,您可以将此类型用于 SimpleCalculator。

在 Visual Basic 中,在 Module1.vb 中添加名为 Program 的公共类。 然后将下面一行添加到 Module1.vb 或 Program.cs 中的 Program 类:

Dim _container As CompositionContainer
private CompositionContainer _container;

为了发现可用于组合容器的部件,组合容器将使用“目录”。 目录就是一个对象,通过它可从某些源发现可用部件。 MEF 提供了用于从提供的类型、程序集或目录发现部件的目录。 应用程序开发人员可以轻松地创建用于从其他源(如 Web 服务)发现部件的新目录。

将下面的构造函数添加到 Program 类:

Public Sub New()
    'An aggregate catalog that combines multiple catalogs
     Dim catalog = New AggregateCatalog()

    'Adds all the parts found in the same assembly as the Program class
    catalog.Catalogs.Add(New AssemblyCatalog(GetType(Program).Assembly))

    'Create the CompositionContainer with the parts in the catalog
    _container = New CompositionContainer(catalog)

    'Fill the imports of this object
    Try
        _container.ComposeParts(Me)
    Catch ex As Exception
        Console.WriteLine(ex.ToString)
    End Try
End Sub
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());
   }
}

ComposeParts 的调用将告知组合容器对一组特定的部件(在此例中为 Program 的当前实例)进行组合。 但是,此时不会发生任何操作,因为 Program 没有要填充的导入。

使用特性的导入和导出

首先,让 Program 导入一个计算器。 这样就可以将用户界面问题(例如,将传递给 Program 的控制台输入和输出)与计算器的逻辑相分离。

将下面的代码添加到 Program 类中:

<Import(GetType(ICalculator))>
Public Property calculator As ICalculator
[Import(typeof(ICalculator))]
public ICalculator calculator;

请注意,虽然 calculator 对象的声明并没有特别之处,但该对象使用 ImportAttribute 特性进行了修饰。 此特性将某个对象声明为一个导入;也就是说,在组合对象时将由组合引擎对它进行填充。

每个导入都有一个协定,用于确定将与之匹配的导出。 协定可以是显式指定的字符串,也可以由 MEF 从给定的类型(在此例中为 ICalculator 接口)自动生成。 使用匹配的协定声明的任何导出都将满足此导入。 请注意,尽管 calculator 对象的类型实际为 ICalculator,但这并不是必需的。 协定与导入对象的类型无关。 (在此例中,您可以忽略 typeof(ICalculator)。 MEF 将会自动假定协定基于导入的类型,除非您显式指定协定将基于的类型。)

将这个非常简单的接口添加到模块或 SimpleCalculator 命名空间中:

Public Interface ICalculator
    Function Calculate(ByVal input As String) As String
End Interface
public interface ICalculator
{
    String Calculate(String input);
}

既然您已定义 ICalculator,就需要一个类来实现它。 将下面的类添加到模块或 SimpleCalculator 命名空间中:

<Export(GetType(ICalculator))>
Public Class MySimpleCalculator
   Implements ICalculator

End Class
[Export(typeof(ICalculator))]
class MySimpleCalculator : ICalculator
{

}

此处是将与 Program 中的导入匹配的导出。 为了使导出与导入匹配,导出必须具有相同的协定。 依据基于 typeof(MySimpleCalculator) 的协定进行导出时会造成不匹配,将不会填充导入;协定需要完全匹配。

因为组合容器将由此程序集中提供的所有部件进行填充,所以 MySimpleCalculator 部件将可用。 当 Program 的构造函数对 Program 对象执行组合操作时,此对象的导入将由特意创建的 MySimpleCalculator 对象填充。

用户界面层 (Program) 不需要知道任何其他内容。 因此,您可以通过 Main 方法来填充用户界面逻辑的其余部分。

将以下代码添加到 Main 方法中:

Sub Main()
    Dim p As New Program()
    Dim s As String
    Console.WriteLine("Enter Command:")
    While (True)
        s = Console.ReadLine()
        Console.WriteLine(p.calculator.Calculate(s))
    End While
End Sub
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));
    }
}

此代码只是读取一行输入,并对结果调用 ICalculator 的 Calculate 函数,该函数将结果写回到控制台。 这就是 Program 中所需的全部代码。 剩下的所有工作将在部件中完成。

进一步的导入和 ImportMany

为了使 SimpleCalculator 能够扩展,它需要导入一组操作。 一般的 ImportAttribute 特性由一个且只由一个 ExportAttribute 填充。 如果有多个导出可用,则组合引擎将生成错误。 若要创建一个可由任意数量的导出填充的导入,可以使用 ImportManyAttribute 特性。

将以下 operations 属性添加到 MySimpleCalculator 类中:

<ImportMany()>
Public Property operations As IEnumerable(Of Lazy(Of IOperation, IOperationData))
[ImportMany]
IEnumerable<Lazy<IOperation, IOperationData>> operations;

Lazy<T, TMetadata> 是 MEF 提供的用于保存对导出的间接引用的类型。 在此,除了导出的对象本身以外,您还将获取导出元数据或描述导出的对象的信息。 每个 Lazy<T, TMetadata> 都包含一个 IOperation 对象(表示一个实际运算)和一个 IOperationData 对象(表示运算的元数据)。

将下面的简单接口添加到模块或 SimpleCalculator 命名空间中:

Public Interface IOperation
    Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer
End Interface

Public Interface IOperationData
    ReadOnly Property Symbol As Char
End Interface
public interface IOperation
{
     int Operate(int left, int right);
}

public interface IOperationData
{
    Char Symbol { get; }
}

在此例中,每个运算的元数据是表示对应运算的符号,如 +、-、* 等。 若要使加法运算可用,请将下面的类添加到模块或 SimpleCalculator 命名空间中:

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "+"c)>
Public Class Add
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left + right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '+')]
class Add: IOperation
{
    public int Operate(int left, int right)
    {
        return left + right;
    }
}

ExportAttribute 特性的功能与以前相同。 ExportMetadataAttribute 特性将元数据以名称/值对的形式附加到相应导出。 尽管 Add 类可实现 IOperation,但并未显式定义实现 IOperationData 的类。 相反,MEF 基于所提供元数据的名称使用属性隐式创建了一个类。 (这是访问 MEF 中的元数据的方法之一。)

MEF 中的组合是递归的。 您显式组合 Program 对象,该对象会导入一个证明是 MySimpleCalculator 类型的 ICalculator。 接下来,MySimpleCalculator 将导入 IOperation 对象的集合,而该导入将在创建 MySimpleCalculator 时与 Program 的导入同时进行填充。 如果 Add 类声明了进一步的导入,还必须填充接下来的导入,以此类推。 任何未填充的导入将导致组合错误。 (不过,可以将导入声明为可选导入或为其分配默认值。)

计算器逻辑

在这些部件准备就绪后,剩下的就只有计算器逻辑本身了。 在 Calculate 类中添加以下代码以实现 MySimpleCalculator 方法:

Public Function Calculate(ByVal input As String) As String Implements ICalculator.Calculate
    Dim left, right As Integer
    Dim operation As Char
    Dim fn = FindFirstNonDigit(input) 'Finds the operator
    If fn < 0 Then
        Return "Could not parse command."
    End If
    operation = input(fn)
    Try
        left = Integer.Parse(input.Substring(0, fn))
        right = Integer.Parse(input.Substring(fn + 1))
    Catch ex As Exception
        Return "Could not parse command."
    End Try
    For Each i As Lazy(Of IOperation, IOperationData) In operations
        If i.Metadata.symbol = operation Then
            Return i.Value.Operate(left, right).ToString()
        End If
    Next
    Return "Operation not found!"
End Function
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!";
}

初始步骤将输入字符串分析为左右操作数和运算符。 在 foreach 循环中,将对 operations 集合的每个成员进行检查。 这些对象的类型是 Lazy<T, TMetadata>,可分别使用 Metadata 属性和 Value 属性来访问其元数据值和导出的对象。 在此例中,如果发现 IOperationData 对象的 Symbol 属性是一个匹配项,则计算器将调用 IOperation 对象的 Operate 方法并返回结果。

若要完成计算器,您还需要一个帮助器方法,该方法返回字符串中第一个非数字字符的位置。 将下面的帮助器方法添加到 MySimpleCalculator 类中:

Private Function FindFirstNonDigit(ByVal s As String) As Integer
    For i = 0 To s.Length
        If (Not (Char.IsDigit(s(i)))) Then Return i
    Next
    Return -1
End Function
private int FindFirstNonDigit(String s)
{
    for (int i = 0; i < s.Length; i++)
    {
        if (!(Char.IsDigit(s[i]))) return i;
    }
    return -1;
}

现在,您应该能够编译和运行项目。 在 Visual Basic 中,确保向 Module1 模块添加了 Public 关键字。 在控制台窗口中,键入一个加法运算(如“5+3”),计算器将返回结果。 任何其他运算符都将导致生成“Operation Not Found!”(运算未找到!)消息。

使用新类扩展 SimpleCalculator

既然计算器能够正常工作,那么添加新运算就很容易了。 将下面的类添加到模块或 SimpleCalculator 命名空间中:

<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left - right
    End Function
End Class
[Export(typeof(IOperation))]
[ExportMetadata("Symbol", '-')]
class Subtract : IOperation
{
    public int Operate(int left, int right)
    {
        return left - right;
    }
}

编译并运行项目。 键入减法运算,如“5-3”。 计算器现在支持减法和加法运算。

使用新程序集扩展 SimpleCalculator

虽然将类添加到源代码相当简单,但是 MEF 提供了在应用程序自身的源外部查找部件的能力。 为了演示这一点,您将需要修改 SimpleCalculator,通过添加 DirectoryCatalog,以便在目录以及目录自己的程序集中搜索部件。

将一个名为 Extensions 的新目录添加到 SimpleCalculator 项目中。 确保将该目录添加到项目级别,而非解决方案级别。 然后,将一个名为 ExtendedOperations 的新类库项目添加到解决方案中。 该新项目将编译到单独的程序集中。

打开 ExtendedOperations 项目的项目属性设计器,然后单击**“编译”“生成”选项卡。 将“生成输出路径”“输出路径”**更改为指向 SimpleCalculator 项目目录中的 Extensions 目录 (.. \SimpleCalculator\Extensions\)。

在 Module1.vb 或 Program.cs 中,将下面一行添加到 Program 构造函数:

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

使用 Extensions 目录的路径替换示例路径。 (此绝对路径仅用于调试目的。 在生产应用程序中,应使用相对路径。)现在,DirectoryCatalog 将在 Extensions 目录下的任何程序集中找到的所有部件都添加到组合容器中。

在 ExtendedOperations 项目中,添加对 SimpleCalculator 和 System.ComponentModel.Composition 的引用。 在 ExtendedOperations 类文件中,为 System.ComponentModel.Composition 添加 Imports 或者 using 语句。 在 Visual Basic 中,还要为 SimpleCalculator 添加 Imports 语句。 然后,将下面的类添加到 ExtendedOperations 类文件中:

<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
    Implements IOperation

    Public Function Operate(ByVal left As Integer, ByVal right As Integer) As Integer Implements IOperation.Operate
        Return left Mod right
    End Function
End Class
[Export(typeof(SimpleCalculator.IOperation))]
[ExportMetadata("Symbol", '%')]
public class Mod : SimpleCalculator.IOperation
{
    public int Operate(int left, int right)
    {
        return left % right;
    }
}

请注意,为了使协定能够匹配,ExportAttribute 特性的类型必须与 ImportAttribute 相同。

编译并运行项目。 测试新 Mod (%) 运算符。

结束语

本主题介绍了 MEF 的基本概念。

  • 部件、目录和组合容器

    部件和组合容器是 MEF 应用程序的基本生成块。 部件是导入或导出值(甚至包括自身)的任何对象。 目录提供来自特定源的部件的集合。 组合容器使用目录提供的部件来执行组合操作(即,将导入绑定到导出)。

  • 导入和导出

    导入和导出是组件通信的方式。 利用导入,组件指定对特定值或对象的需要,利用导出,组件指定值的可用性。 每个导入都按其协定的方式与一组导出匹配。

现在转到什么位置?

若要下载本示例的完整代码,请参见 SimpleCalculator sample(SimpleCalculator 示例)。

有关更多信息和代码示例,请参见 Managed Extensibility Framework。 有关 MEF 类型的列表,请参见 System.ComponentModel.Composition 命名空间。

修订记录

日期

修订记录

原因

2010 年 7 月

更新了步骤。 添加了适用于 VB 的缺失步骤。 添加了指向下载示例的链接。

客户反馈