MEF 2 预览版入门指南






4.84/5 (13投票s)
简要指南,介绍MEF下一版本中值得期待的新特性。
引言
今年,所有.NET开发者都将为我们钟爱的框架的又一个新版本发布所带来的兴奋和期待而欢欣鼓舞。所以,本着追求新颖和令人兴奋的精神,今天我将讨论我最喜欢的MEF之一。我们都知道MEF在日益复杂的开发环境中能够带来极大的实现简便性。首先,我将讨论到目前为止MEF2新增的关键特性,我认为这些特性将在.NET 4.5发布时产生巨大影响。但是请注意,这更像是一个预览,我展示的内容并非全部都会包含在最终版本中。截至本文撰写时,当前MEF 2版本为预览版。将要讨论的新特性包括 `ExportFactory<T>` 和一个名为 `RegistrationBuilder` 的新注册API,仅举几例。那么,让我们开始吧。
背景
这是一份入门指南,旨在帮助您快速熟悉MEF 2中已实现的一些关键变化,以便您可以立即开始使用预览版本,而无需额外的研究和理解。
设置
首先,对于那些已经使用过MEF第一版的开发者来说,您会注意到新版本和即将发布的版本之间有一些显著的区别。其中一个细微的变化是所有通用的导出目录(export catalogs)现在都包含了一个额外的构造函数重载。这个参数是用于一个新类 `RegistrationBuilder` 的。这个类通过 `RegistrationBuilder` 丰富的API处理所有导出项的注册和部件(part)的创建。因此,您不再需要担心在所有依赖项上放置属性。这是MEF开发团队的一个重大举措,因为MEF的主要作用是提供一个简单的实现方式,用于松散耦合架构和可扩展项目的依赖注入类型。任何扩展项目功能的开发者都不必再担心设置他/她希望注入的对象的属性。下面让我们看一个示例设置;正如您在下面的代码片段中所见,MEF在其对象生命周期管理以及我上面提到的其他新特性方面更加灵活。现在,我猜您已经听腻了我喋喋不休地唠叨,让我向您展示容器和一个简单导出的设置方法。
var convention = new RegistrationBuilder();
//export a type implementing an interface.
convention.ForTypesDerivedFrom<igreetings>().Export<greetings>();
正如您所见,我们实例化了新的 `RegistrationBuilder` 类,然后使用其Fluent API来映射依赖项。在这个简单的例子中,导出一个实现了接口的具体类型。对于那些没有使用过MEF 1的开发者来说,在MEF 1中等效的做法如下。[InheritedExport] //MEF 1
public interface IGreetings
{
void Greet(string name);
}
public class Greetings : IGreetings
现在,注册容器如下。
var assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly(), registeredExports);
var catalog = new AggregateCatalog(assemblyCatalog);
container = new CompositionContainer(catalog,
CompositionOptions.DisableSilentRejection | CompositionOptions.IsThreadSafe);
container.ComposeParts(this);
请注意,我们现在将 `RegistrationBuilder` 的实例变量 `registeredExports` 传递给 `assemblyCatalog` 的构造函数。然后,其余过程在实践中与MEF 1几乎相同。但还有一行需要讨论。 `CompositionContainer` 的构造函数包含一个用于枚举 `CompositionOptions` 的重载,用于组合部件(composing parts)。
container = new CompositionContainer(catalog, CompositionOptions.DisableSilentRejection | CompositionOptions.IsThreadSafe);
可用选项有
CompositionOption | 描述 |
默认值 | 是默认的实现和设置。 |
DisableSilentRejection |
当导入/导出失败时,显示更详细的异常。 |
IsThreadSafe | 以线程安全的方式组合导出项。 |
ExportCompositionService |
设置导出组合服务。 |
这些选项的目的是更好地管理目录部件(catalog parts)的组合,并提供更具信息量的异常,以便更好地诊断失败的导出项。顺便说一句,这在MEF 1中是个噩梦。正如您可能已经注意到的,MEF 2在设置和导入导出项的方式上要灵活得多。那么,让我们将讨论的重点转移到这一点,并剖析 `RegistrationBuilder` 的API,以进一步理解导出项是如何注册、导入到类型中,并最终被容器使用的。
以灵活的方式注册类型
MEF有三种方式来注册一个特定类型并将其导出。
-
ForType()
&ForType<>()
:允许您指定您希望导出的任何类型,无论是通过泛型方法在编译时已知类型。还是通过传递一个仅在运行时才知的对象的类型。convention.ForType<test>()
-
ForTypesMatching()
&ForTypesMatching<>()
:允许您根据谓词过滤器(predicate filter)导出类型。convention.ForTypesMatching(x => x.Name.EndsWith("Exam")).Export();
-
ForTypesDerivedFrom<>()
&ForTypesDerivedFrom()
:允许您指定一个对象继承的基类或接口,然后导出它。这与在MEF 1中使用 `InheritedExport` 相同。这是导出类型的首选且最具扩展性的方法。因为它会解析实现指定接口或继承其基类的所有具体类型。这最终意味着您不必在注册导出项时指定具体类型。MEF会自动完成。这将找到所有实现 `IGreeting` 的具体实现,在本例中是 `Greeting`。 convention.ForTypesDerivedFrom<IGreetings>().Export<IGreetings>();
使用API
我们已经简要了解了每个方法在MEF中注册类型作为导出项的作用。现在,让我们将理论付诸实践。我创建了一个简单的项目,并在本文中提供了它,名为 `Learning.MEFII.Console.Exams`。项目的类结构如下。
基本场景是一个学习者根据是否提供考试教材来撰写特定的考试。这个例子将说明如何使用 `ForTypesMatching()` 方法基于类型信息导出和导入某些对象。以及简要介绍导入导出项集合。
class Program
{
public CompositionContainer container;
static void Main(string[] args)
{
var p = new Program();
p.RegisterExports();
var exams = p.container.GetExportedValue<learner>();
}
public void RegisterExports()
{
var builder = new RegistrationBuilder();
builder.ForType<learner>().Export<learner>();
builder.ForTypesMatching
(x => x.GetProperty("SourceMaterial") != null).Export<exam>();
InitializeMef(builder);
}
public void InitializeMef(RegistrationBuilder registeredExports)
{
var assemblyCatalog =
new AssemblyCatalog(Assembly.GetExecutingAssembly(), registeredExports);
var catalog = new AggregateCatalog(assemblyCatalog);
container = new CompositionContainer(catalog,
CompositionOptions.DisableSilentRejection | CompositionOptions.IsThreadSafe);
container.ComposeParts(this);
}
}
在上面的代码示例中,您可以看到在 `Program` 主类中,我们像以前一样注册了容器和一个名为 `Learner` 的简单导出项。但是,有一个第二个注册的导出项是以一种非常不同的方式注册的。那就是 `Exam` 对象的导出。
builder.ForTypesMatching(x => x.GetProperty("SourceMaterial") != null).Export<Exam>();
您可以看到我只导出包含 `SourceMaterial` 属性的类。这是一个非常简单的场景,说明如何在实际应用程序中根据标准确定某些导出项。但这只是一个简单的例子,说明了MEF 2过滤特定导出项的能力。可以将其视为对您的IOC容器进行LINQ化。
MEF的“语法糖”,锦上添花
MEF2最显著的灵活性体现在 `RegistrationBuilder` API中新增的谓词过滤器,以及对链式方法(chained methods)的广泛使用,这些方法逻辑上关联了配置过程的每一步,以便注册您想要的导出项。MEF的大部分lambda表达式都关联到.NET内置的反射库,这对于任何使用MEF的人来说都应该是一个优势。因为它可以解决学习全新的反射库来操作MEF导出项的问题。正如我在上面的例子中所示,大多数lambda表达式都会传递一个 `System.Type` 参数,并根据指定的标准返回一个 `Boolean` 结果。因此,任何返回true的类型,根据您的过滤器,都将生成一个导出项。这是一个非常强大的选项。它为您提供了更细粒度的依赖注入方法。让我们通过如何选择在MEF中导入导出项时初始化哪个构造函数来进一步阐述这一点。
戴上安全帽,来处理一些对象构造
让我为您描绘以下场景。学习者刚刚开始高中学习,并且要到下个学期才会参加考试。但是,学习者已经报名学习地理,他/她需要一位老师来教他们。所以对象如下。
public Learner(IEnumerable<Exam> exams)
{
Exams = exams;
Name = "Jack";
LastName = "Sparrow";
}
public Learner(IEnumerable<Educator> teacher)
{
Teacher = teacher.OfType<Teacher>().First();
}
有两个构造函数,参数数量相同。除了一个区别,学习者要么只有一位老师来学习特定科目,要么报名参加考试。现在,如果我们有一个学习者实例会发生什么?
发生组合错误(Composition error),因为MEF无法决定使用哪个构造函数来初始化学习者对象。那么,我们如何确定应该使用哪个构造函数呢? `RegistrationBuilder` 的API有一个名为 `SelectConstructor()` 的方法,它接受一个lambda表达式来确定对象初始化时使用的构造函数。因此,代码如下。
Func<ConstructorInfo[], ConstructorInfo> constructorFilter
= x => x.First(z => z.GetParameters()
.First().ParameterType == typeof(IEnumerable<Educator>));
builder.ForType<Learner>().SelectConstructor(constructorFilter).Export<Learner>();
上面,我们调用了 `SelectConstructor()` 方法,并传递了一个lambda,该lambda接受一个 `ConstructorInfo` 数组并返回正确的构造函数。在本例中,我们选择第一个构造函数,它有一个参数类型为 `IEnumerable` of `Educator`,然后在lambda表达式中返回它。因此,结果是调用了正确的构造函数。
另一个需要注意的重要事项是,如果一个基类型被多个类型实现,可以通过将其作为 `IEnumerable` of 您的指定基类型传递,轻松地将其导入到对象的构造函数中。这样做的优点是,您无需在API中进行任何进一步的配置即可实现此功能。MEF还有一些开箱即用的功能。首先,当您将导入项注入到具有多个构造函数的对象中时,将选择参数最多的那个构造函数来构造对象。延迟加载(Lazy loading)也可以无需配置即可实现,您只需遵循延迟加载导入的要求即可。这可以通过将类型包装在 `Lazy<T>` 对象中来完成。下面我将举例说明。
看,所有这一切都可以通过遵循我讨论的约定,并编写最少的代码来默认实现。
builder.ForType<Learner>().Export<Learner>();
builder.ForTypesDerivedFrom<Educator>().Export<Educator>();
延迟的好处
我在前一个例子中展示的第一个问题是。`Lazy<T>` 类是什么?它对我有什么好处?`Lazy<T>` 是一个包装类,它延迟对象的实例化,直到需要使用时。我们都想知道如何通过最小化昂贵的调用对象或缓存信息来限制对特定对象的调用次数,而不必每次调用时都实例化一个新实例,从而提高应用程序的性能。是的,可以创建一个对象的静态单例,并在应用程序的整个生命周期中调用它。但是,如果一个对象只在某些场景下被调用怎么办?例如,一个支持插件的应用程序,您应该只实例化用户选择的插件,而不是在应用程序启动时实例化所有插件。资源应该只在需要时实例化。实例化所有对象,而不管它们是否会被使用,这种做法的问题是,内存中总会有不必要的对象存在,这是低效且资源密集型的。
这就是 `Lazy<T>` 类解决这个问题的方式,而无需您编写一个单例对象,然后编写一些逻辑来以线程安全的方式处理对象初始化的延迟。所有这些都为您完成,只需将类型包装在 `Lazy<T>` 泛型参数中即可。`Lazy<T>` 是如何工作的?`Lazy<T>` 有两个主要属性:`Value` 和 `IsValueCreated`。第一次调用 `Value` 属性时,会使用 `Activator.CreateInstance()` 方法通过动态实例化对象来创建一个对象实例。此后,每次调用 `Lazy<T>.Value` 时,都会调用当前缓存的实例。因此,对象只初始化一次,`IsValueCreated` 会被设置为true。使用 `Lazy<T>` 类时需要考虑一些事项。反射可能会使您的代码更灵活和动态,但这确实是有代价的。所有与反射相关的细节只在运行时才知道,因此使用反射进行的任何任务总是比在编译时直接实现或调用要慢。
在这种情况下,直接调用
Exam exam = new Exam();
将始终比
Activater.CreateInstance(typeof(Exam));
更快。话虽如此,MEF实例化所有类型都是基于运行时的,所以在这种情况下,所有导出项都会在运行时组合,因此将您的导入项作为 `Lazy<T>` 对象传递没有任何区别。最后,一个不相关的注意事项是,如果您在构造函数中调用 `Lazy<T>` 的实例,您可以指定您希望某个对象如何初始化。
例如
Lazy<Learner> lazyLearner = new Lazy<Learner>(()
=> new Learner(p.container.GetExportedValues<Educator>().OfType<Teacher>().First()));
var learner = lazyLearner.Value;
看,结果与之前相同,但通过控制 `Value` 属性来控制使用哪个构造函数实例化对象。还有一个重载可以通过传递一个布尔值来控制对象的线程安全。
对象生命周期
在最后几节中,我们学习了如何使用 `RegistrationBuilder` 的导入和导出API,通过即时注入以及 `Lazy<T>` 实现的延迟注入来构造对象实例。但是,这些新创建实例的生命周期如何?它们将如何管理?好了,现在让我们通过关注这些新创建实例生命周期的重要性来解决这个问题。当处理控制反转容器和依赖注入时,这通常是一个被忽视的问题,因为我们必须承认,在某些情况下,创建对象的单例是不必要的,并且会消耗大量资源。
PartCreationPolicy是如何工作的
在MEF1中,您需要通过在导出项上使用一个属性来处理部件(part)的创建。这样,当该对象在另一个导出对象中被导入时,默认会返回该对象的单例。然而,如果您明确将 `partcreationpolicy` 设置为 `NonShared`,每次导入该对象时都会创建一个新实例。让我们打开 `Learning.MEFII.Console.ObjLifetime` 项目来说明这一点。场景如下:一个 `LifetimeManager` 对象通过4次调用迭代导出对象 `ObjectTester`,并设置其计数器属性,然后返回结果。
public class ObjectTester
{
public int Counter { get; set; }
public ObjectTester()
{
}
}
public class LifeTimeManager
{
public void ShowObjLifetime(CompositionContainer container)
{
for (int i = 1; i < 5; i++)
{
var tester = container.GetExportedValue<ObjectTester>();
++tester.Counter;
System.Console
.WriteLine("execution {0} Counter Value is {1}", i,tester.Counter);
}
}
}
请注意上面的结果表明,无论 `ObjectTester` 导入项被调用多少次,在第一次实例化后它都不会再次被构造。这就是为什么计数器值每次都会增加,因为默认情况下您始终使用相同的实例。我们如何改变这一点?再次转向 `RegistrationBuilder` 的API,在 `Export()` 方法之后,指定 `SetCreationPolicy()` 方法。并将其 `creationPolicy` 枚举设置为 `NonShared`。像这样。
现在我们得到了期望的结果:每次在循环中调用导入时,都会得到一个新的实例。这只需要改变一行代码即可实现。这最终意味着您不必像在MEF 1中那样担心更改代码或添加属性。
共享还是不共享
我们知道MEF2支持更改对象的生命周期,但在哪些场景下它是有用的?我们为什么要覆盖它默认导入单例到依赖对象行为?首先,最常见的场景是 `DbContext`。当您希望存储库(repository)每次执行数据库查询时都获得一个新的 `DbContext` 实例。但是,如果您想控制何时必须实例化该实例并将其处置呢?
ExportFactory<t>
`ExportFactory<T>` 提供了对对象生命周期的更好控制。当您需要生成实例以及何时处置该实例时。而不是在父对象生成时生成对象,您可以只在需要时访问实例。例如 `Lazy<T>`,不同之处在于您可以在执行完成后处置该实例。这有点像一个工作单元(unit of work)的作用域导入。让我们通过一个模拟数据库查询的例子来看看如何使用它。项目的结构如下。
上面我们有一个简单的项目,模拟了一个从 `DbContext` 中获取数据的存储库实现,就像Entity Framework一样,除了在这个例子中数据源是硬编码的。
public class PersonRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IPerson
{
public ExportFactory<IMockDbContext> DbContext { get; set; }
public PersonRepository(ExportFactory<IMockDbContext> dbContext)
{
DbContext = dbContext;
}
public TEntity GetByAge(Func<IPerson, bool> filter)
{
using (var ctx = DbContext.CreateExport())
{
return ctx.Value.MockPersonDbSet.AsEnumerable().SingleOrDefault(filter) as TEntity;
}
}
}
上面需要注意的第一个重要事情是 `IMockDbContext` 对象被包装在一个 `ExportFactory<T>` 对象中,然后被注入到 `Person Repository` 类中。然后,当调用 `GetByAge` 方法时。会使用一个 `using` 语句包装 `ExportFactory<T>` 类型,并调用 `CreateExport()` 方法。这立即表明导出项实现了 `IDisposable`,并在代码块执行完成后处置导出项。该对象仅在调用 `ExportLifetime<IMockDbContext>` 对象 ctx 的 `Value` 属性时才被初始化。然后,该对象在完成从 `MockDbContext` 获取最旧的 `Person` 后会被处置,这要归功于 `using` 块。
public class ProgramManager
{
public IRepository<IPerson> PersonRepo { get; set; }
public ProgramManager(IRepository<IPerson> personRepo)
{
PersonRepo = personRepo;
}
public void SimulateUserAction()
{
var person = PersonRepo.GetByAge(x => x.Age > 21);
System.Console.WriteLine("The oldest person in the table is {0} at the tender age of {1}", person.Name, person.Age);
System.Console.ReadKey();
}
}
为什么使用 ExportFactory<T>?
`ExportFactory<T>` 的好处取决于您的应用程序的架构以及您应用程序定义的特定需求。例如, `ExportFactory<T>` 将用于最小化对象创建的资源密集度,只在需要使用该对象时才调用它,并在使用后处置该对象。`Lazy<T>` 是延迟创建对象的绝佳例子,但是,您在控制对象的生命周期方面没有更精细的控制。一旦在 `Lazy<T>` 上调用 `Value` 属性来初始化对象,它将一直保留在内存中,直到应用程序生命周期结束。 `ExportFactory<T>` 略有不同,因为它延迟了对象的实例化,并为您提供了控制对象实例处置的功能。
导出多个接口
MEF 2 预览版 5 现在支持在已实现的具体类型上导出多个接口。通过 `RegistrationBuilder` API中的 `ExportInterfaces()` 方法。有三种方法重载:第一种用于导出所有已实现的接口,第二种重载用于仅导出满足特定标准的接口,最后第三种重载接受一个lambda,该lambda指定 `ExportBuilder` 和接口过滤器。
在下面的例子中,我们将使用第一种和第二种重载。请注意,我们使用的项目名为 `Learning.MEFII.Console.MultipleInterfaces`
这里有一个简单的程序,它展示了由各个相关接口组成的,构成人类特征的组成部分。 `Person` 类实现了所有这些接口,因为它们都与 `Person` 对象相关。
代码如下。
public class Person : IHuman, IMammal, IHomosapien
{
public int NoLegs { get; set; }
public HairType HairType { get; set; }
public long IdNumber { get; set; }
public string Name { get; set; }
public Person()
{
NoLegs = 2;
HairType = MultipleInterfaces.HairType.Hair;
IdNumber = 8404295531083;
Name = "Butch";
}
public string BloodType { get; set; }
}
static void Main(string[] args)
{
var p = new Program();
p.RegisterExports();
var humanCharacteristics = p.container.GetExportedValue<IHuman>();
var mammalCharacteristics = p.container.GetExportedValue<IMammal>();
System.Console.WriteLine("Human Characteristics Name: {0} Id Number: {1}",
humanCharacteristics.Name, humanCharacteristics.IdNumber);
System.Console.WriteLine("Mammal Characteristics No of Legs: {0} Type of Hair: {1}",
mammalCharacteristics.NoLegs, mammalCharacteristics.HairType);
System.Console.ReadKey();
}
public void RegisterExports()
{
RegistrationBuilder builder = new RegistrationBuilder();
builder.ForType<Person>()
.ExportInterfaces();
InitializeMef(builder);
}
如您所见,所有实现 `Person` 对象的接口都将被导出。因此,当请求指定契约类型(例如接口)的已导出值时。只有与该已实现接口相关的属性和方法才会从 `Person` 对象暴露给客户端。请注意,即使我正在导出和检索多个接口的值。该对象默认仍然只作为一个单例创建。默认情况下,第一个调用具体类型的导出接口将实例化该对象。例如:
结论
本文介绍了MEF下一迭代版本中可能发布的一些令人兴奋的新特性,该版本将在今年晚些时候随.NET 4.5一同发布。最显著的变化是取消了属性驱动的注入,以及一个更丰富的API来处理导出项的定义和导入项的注入。它在定义可注册到 `CompositionContainer` 的类型的标准方面也提供了更大的灵活性。MEF项目不断壮大,MEF和.NET框架的前景一片光明。所以请关注此领域。
关注点
查看此链接。
http://blogs.msdn.com/b/bclteam/archive/2011/12/19/what-s-new-in-mef-2-preview-5-alok-nick.aspx
历史
版本 0.1 Dean Oliver 著