本文概述了 .NET Framework 4 中引入的托管扩展性框架。
什么是 MEF?
托管扩展性框架(MEF)是用于创建轻型和可扩展应用程序的库。 它允许应用程序开发人员发现和使用扩展,无需配置。 它还允许扩展开发人员轻松封装代码,并避免易碎的硬依赖项。 MEF 不仅允许在应用程序中重复使用扩展,还允许跨应用程序重复使用扩展。
扩展性问题
假设你是大型应用程序的架构师,它必须提供扩展性支持。 应用程序必须包含可能大量较小的组件,并负责创建和运行它们。
问题最简单的方法是将组件作为源代码包含在应用程序中,并直接从代码调用它们。 这有一些明显的缺点。 最重要的是,你无法在不修改源代码的情况下添加新组件,这一限制在例如 Web 应用程序中可能是可以接受的,但在客户端应用程序中是无法适用的。 同样有问题,你可能无权访问组件的源代码,因为它们可能是由第三方开发的,并且出于相同的原因,你不能允许他们访问你的。
稍微复杂的方法是提供扩展点或接口,以允许在应用程序与其组件之间分离。 在此模型中,可以提供组件可以实现的接口,并提供一个 API,使它能够与应用程序交互。 这解决了需要源代码访问的问题,但它仍然有其自身的困难。
由于应用程序本身缺少发现组件的任何容量,因此它仍必须明确告知哪些组件可用且应加载。 这通常是通过显式注册配置文件中的可用组件来实现的。 这意味着,确保组件正确成为维护问题,特别是如果是最终用户,而不是预期执行更新的开发人员。
此外,组件无法相互通信,只能通过应用程序本身严格定义的渠道。 如果应用程序架构师没有预料到需要特定通信,则通常是不可能的。
最终,组件开发人员必须接受包含他们实现的接口的程序集所在的硬依赖项。 这使得组件难以在多个应用程序中使用,并且也可以在为组件创建测试框架时创建问题。
MEF 提供的内容
MEF 提供了一种通过 组合隐式发现它们的方法,而不是对可用组件进行显式注册。 MEF 组件(称为 部件)以声明方式指定其依赖项(称为 导入),以及它提供的功能(称为 导出)。 创建部件时,MEF 组合机制会通过其他部件提供的资源来满足该部件的导入需求。
此方法解决了上一节中讨论的问题。 由于 MEF 部件以声明方式指定其功能,因此可以在运行时发现它们,这意味着应用程序可以在不使用硬编码引用或脆弱的配置文件的情况下使用部件。 MEF 允许应用程序通过元数据发现和检查部件,而无需实例化它们,甚至加载程序集。 因此,无需仔细指定何时以及如何加载扩展。
除了提供的输出之外,部件还可以指定其输入,这些输入将由其他部件填充。 这使得各部分之间的通信不仅成为可能,而且很容易,并允许对代码进行良好的分解。 例如,许多组件的通用服务可以分解为单独的部件,并轻松修改或替换。
由于 MEF 模型不需要对特定应用程序程序集硬依赖,因此它允许将扩展从应用程序重用到应用程序。 这也使开发独立于应用程序的测试框架变得容易,用来测试扩展组件。
使用 MEF 编写的可扩展应用程序声明可由扩展组件填充的导入,还可以声明导出,以便向扩展公开应用程序服务。 每个扩展组件声明导出,还可以声明导入。 这样,扩展组件本身就会自动扩展。
MEF 的可用地点
MEF 是 .NET Framework 4 不可或缺的一部分,可在使用 .NET Framework 的任何位置使用。 无论是使用 Windows 窗体、WPF 或任何其他技术的客户端应用程序,还是使用 ASP.NET 的服务器应用程序,都可以使用 MEF。
MEF 和 MAF
早期版本的 .NET Framework 引入了托管外接程序框架(MAF),旨在允许应用程序隔离和管理扩展。 MAF 的重点略高于 MEF,专注于扩展隔离和程序集加载和卸载,而 MEF 的重点是可发现性、可扩展性和可移植性。 这两个框架可顺利互作,单个应用程序可以利用这两者。
SimpleCalculator:示例应用程序
了解 MEF 能做什么的最简单方法是构建一个简单的 MEF 应用程序。 在此示例中,你将生成一个名为 SimpleCalculator 的非常简单的计算器。 SimpleCalculator 的目标是创建一个控制台应用程序,以“5+3”或“6-2”的形式接受基本算术命令,并返回正确的答案。 使用 MEF,无需更改应用程序代码即可添加新运算符。
若要下载此示例的完整代码,请参阅 SimpleCalculator 示例(Visual Basic)。
注释
SimpleCalculator 的目的是演示 MEF 的概念和语法,而不是一定提供实际方案供其使用。 许多从 MEF 的强大功能中受益的应用程序比 SimpleCalculator 更复杂。 有关更广泛的示例,请参阅 GitHub 上的 托管扩展性框架 。
若要开始,请在 Visual Studio 中创建新的控制台应用程序项目并将其命名
SimpleCalculator
。添加对 MEF 所在的
System.ComponentModel.Composition
程序集的引用。打开Module1.vb或Program.cs,并为
Imports
和using
添加System.ComponentModel.Composition
或System.ComponentModel.Composition.Hosting
指令。 这两个命名空间包含需要开发可扩展应用程序的 MEF 类型。如果使用 Visual Basic,请将
Public
关键字添加到声明模块的Module1
行。
撰写容器和目录
MEF 组合模型的核心是 合成容器,其中包含所有可用部件和执行合成。 组合是对导入到导出进行的匹配。 最常见的组合容器类型是 CompositionContainer,你将对 SimpleCalculator 使用此类型。
如果使用 Visual Basic,请添加Program
的公共类。
将以下行添加到Program
Module1.vb或Program.cs中的类:
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 CompositionException
Console.WriteLine(ex.ToString)
End Try
End Sub
private Program()
{
try
{
// 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);
_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(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()
' Composition is performed in the constructor.
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)
{
// Composition is performed in the constructor.
var p = new Program();
Console.WriteLine("Enter Command:");
while (true)
{
string s = Console.ReadLine();
Console.WriteLine(p.calculator.Calculate(s));
}
}
该代码仅读出一行导入并在结尾调用 Calculate
的 ICalculator
函数(由代码写回控制台)。 这就是Program
中您所需的全部代码。 剩余工作将在部件中进行。
Imports 和 ImportMany 属性
为了使 SimpleCalculator 可扩展,它需要导入一个操作列表。 一般 ImportAttribute 属性由且只能由 ExportAttribute 填写。 如果有多个可用,则合成引擎将产生错误。 若要创建可由任意数量的导出填充的导入,可以使用该 ImportManyAttribute 属性。
将以下操作属性添加到 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(left As Integer, 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(left As Integer, 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
对象(导入了结果为 ICalculator
类型的 MySimpleCalculator
)。 反过来,MySimpleCalculator
导入一组 IOperation
对象,且该导入将在创建 MySimpleCalculator
时进行填写,同时进行 Program
的导入。 如果 Add
类声明了一项进一步的导入,则该导入也须进行填写,等等。 任何未填充的导入都会导致组合错误。 (但是,可以声明导入为可选或为其分配默认值。
计算器逻辑
有了这些部分,剩下的就是计算器逻辑本身。 在 MySimpleCalculator
类中添加以下代码以实现 Calculate
该方法:
Public Function Calculate(input As String) As String Implements ICalculator.Calculate
Dim left, right As Integer
Dim operation As Char
' Finds the operator.
Dim fn = FindFirstNonDigit(input)
If fn < 0 Then
Return "Could not parse command."
End If
operation = input(fn)
Try
' Separate out the operands.
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;
// Finds the operator.
int fn = FindFirstNonDigit(input);
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 访问导出的对象。 在这种情况下,如果发现 Symbol
对象的IOperationData
属性是匹配项,计算器将调用Operate
对象的IOperation
方法并返回结果。
若要完成计算器,还需要一个帮助程序方法,该方法返回字符串中第一个非数字字符的位置。 将以下帮助程序方法添加到 MySimpleCalculator
类:
Private Function FindFirstNonDigit(s As String) As Integer
For i = 0 To s.Length - 1
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 中,请确保已将 Public
关键字添加到 Module1
。 在控制台窗口中,键入加法作,例如“5+3”,计算器返回结果。 任何其他运算符都会导致出现“找不到运算!”消息。
使用新类扩展 SimpleCalculator
现在计算器正常运行后,添加新操作非常简单。 将以下类添加到模块或 SimpleCalculator
命名空间:
<Export(GetType(IOperation))>
<ExportMetadata("Symbol", "-"c)>
Public Class Subtract
Implements IOperation
Public Function Operate(left As Integer, 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 提供了查看应用程序自身源代码之外部件的功能。 为了演示它,将需要通过添加 DirectoryCatalog 来修改简单计算器从而为部件搜索目录以及其自身的程序集。
在 SimpleCalculator 项目中添加一个名为Extensions
的新目录。 请确保将其添加到项目级别,而不是在解决方案级别添加。 然后将一个新的类库项目添加到名为 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
类文件中,添加Imports
指令或using
指令用于System.ComponentModel.Composition
。 在 Visual Basic 中,也为 Imports
添加一个 SimpleCalculator
语句。 然后将以下类添加到 ExtendedOperations
类文件:
<Export(GetType(SimpleCalculator.IOperation))>
<ExportMetadata("Symbol", "%"c)>
Public Class Modulo
Implements IOperation
Public Function Operate(left As Integer, 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 示例(Visual Basic)。
有关详细信息和代码示例,请参阅 托管扩展性框架。 有关 MEF 类型的列表,请参阅 System.ComponentModel.Composition 命名空间。