CLR

无特性的 MEF 配置方法

Alok Shriram

 

Managed Extensibility Framework (MEF) 旨在为 Microsoft .NET Framework 开发人员提供一种简便的方法来构建松散耦合的应用程序。 MEF 版本 1 的主要重点是可扩展性,以使应用程序开发人员可以向第三方开发人员公开某些扩展点,并使第三方开发人员可以构建这些组件的加载项或扩展。 用于扩展 Visual Studio 本身的 Visual Studio 插件模型就是一个很好的使用案例,您可以阅读 MSDN 库页面“开发 Visual Studio 扩展”(bit.ly/IkJQsZ) 以了解相关信息。 这种公开扩展点和定义插件的方法使用所谓的特性化编程模型,开发人员可以使用特性修饰属性、类甚至方法,以通告需要具有特定类型的依赖关系或能够满足特定类型的依赖关系。

尽管特性在具有开放类型系统的可扩展性方案中非常有用,但对于生成时已知的封闭类型系统来说有些多余。 特性化编程模型的一些基本问题包括:

  1. 很多类似部件的配置包括一些不必要的重复内容;这违反了“切勿重复”(DRY) 原则,实际上可能会导致人为错误以及源文件更难以阅读。
  2. 编写 .NET Framework 4 中的扩展或部件意味着依赖于 MEF 程序集,这会将开发人员束缚到特定的依赖关系注入 (DI) 框架。
  3. 设计时没有考虑 MEF 的部件需要添加特性才能在应用程序中正确识别它们。 这可能会对采用构成巨大的障碍。

.NET Framework 4.5 提供了一种集中进行配置的方法,以便可以编写一组规则以说明如何创建和编写扩展点和组件。 这是使用一个名为 RegistrationBuilder (bit.ly/HsCLrG) 的新类实现的,可以在 System.ComponentModel.Composition.Registration 命名空间中找到该类。 在本文中,我首先介绍一些使用 MEF 等系统的原因。 如果您是一个经验丰富的 MEF 开发人员,则可以跳过此部分。 然后,我将扮演分配了一组要求的开发人员角色,并使用 MEF 特性化编程模型创建一个简单的控制台应用程序。 接下来,我将该应用程序转换为基于约定的模型,从而说明如何使用 RegistrationBuilder 实现一些典型方案。 最后,我将讨论如何将约定驱动的配置加入到应用程序模型中,以及它如何将使用 MEF 和现成的 DI 准则变成一件非常简单的事情。

背景

随着软件项目大小和规模的扩大,可维护性、可扩展性和可测性将成为关键问题。 随着软件项目变得越来越成熟,可能需要更换或改进一些组件。 随着项目范围的扩大,要求常常会有所改变或需要增添一些要求。 能否以简单方式在大型项目中添加功能对于该产品的发展极其重要。 再者,由于变化在大多数软件周期内是很平常的事,能够独立于其他组件快速测试软件产品包含的组件是至关重要的,尤其是在并行开发依赖组件的环境中。

正是有了这些推动力,DI 概念在大型软件开发项目中迅速得到推广。 DI 的基本原理是开发组件以通告它们所需的依赖关系(而不实际实例化它们)以及它们满足的依赖关系,并且依赖关系注入框架将确定正确的依赖关系实例并将其“注入”到组件中。 如果您需要了解更多背景信息,2005 年 9 月一期的 MSDN 杂志“依赖关系注入” (msdn.microsoft.com/magazine/cc163739) 是一个极好的资源。

应用场景

现在,让我们介绍一下前面提到的应用场景: 我是一个需要遵循提供的规范的开发人员。 在较高的层次,我要实现的解决方案目标是,根据邮政编码为用户提供天气预报信息。 下面是所需的步骤:

  1. 应用程序要求用户提供邮政编码。
  2. 用户输入一个有效的邮政编码。
  3. 应用程序联系 Internet 气象服务提供程序以获取预报信息。
  4. 应用程序向用户显示设置了格式的预报信息。

从要求角度看,此时显然还存在一些未知问题,或某些方面可能会在周期后期发生变化。 例如,我还不知道要使用哪个气象服务提供程序,或使用什么方法从提供程序中获取数据。 因此,要开始设计该应用程序,我将产品分为几个离散的功能单元: WeatherServiceView、IWeatherServiceProvider 和 IDataSource。 图 1图 2图 3 分别显示了其中的每个类的代码。

图 1 WeatherServiceView — 结果显示类

[Export]
public class WeatherServiceView
{
  private IWeatherServiceProvider _provider;
  [ImportingConstructor]
  public WeatherServiceView(IWeatherServiceProvider providers)
  {
    _providers = providers;
  }
  public void GetWeatherForecast(int zipCode)
  {
    var result=_provider.GetWeatherForecast(zipCode);
      // Some display logic
  }
}

图 2 IWeatherServiceProvider (WeatherUnderground) 数据分析服务

[Export(typeof(IWeatherServiceProvider))]
class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{  private IDataSource _source;
  [ImportingConstructor]
  public WeatherUndergroundServiceProvider(IDataSource source)
  {
    _source = source;
  }
  public string GetWeatherForecast(int zipCode)
  {
    string val = _source.GetData(GetResourcePath(zipCode));
      // Some parsing logic here
    return result;
  }
  private string GetResourcePath(int zipCode)
  {
    // Some logic to get the resource location
  }
}

图 3 IDataSource (WeatherFileSource)

[Export(typeof(IDataSource))]
class WeatherFileSource :IDataSource
{
  public string GetData(string resourceLocation)
  {
    Console.WriteLine("Opened ----> File Weather Source ");
    StringBuilder builder = new StringBuilder();
    using (var reader = new StreamReader(resourceLocation))
    {
      string line;
      while((line=reader.ReadLine())!=null)
      {
        builder.Append(line);
      }
    }
    return builder.ToString();
  }
}

最后,为了创建这种部件层次结构,我需要使用一个 Catalog,可以通过它查找应用程序中的所有部件,然后使用 CompositionContainer 获取 WeatherServiceView 实例,可随后对该实例进行处理,如下所示:

class Program
{
  static void Main(string[] args)
  {
    AssemblyCatalog cat = 
      new AssemblyCatalog(typeof(Program).Assembly);
    CompositionContainer container = 
      new CompositionContainer(cat);           
    WeatherServiceView forecaster =
      container.GetExportedValue<WeatherServiceView>();
    // Accept a ZIP code and call the viewer
    forecaster.GetWeatherForecast(zipCode);
  }
}

我此前介绍的所有代码都是非常基本的 MEF 语义;如果您不清楚其中的任何代码的工作原理,请参阅 MSDN 库页面“Managed Extensibility Framework 概述”(bit.ly/JLJl8y),其中详细介绍了 MEF 特性化编程模型。

约定驱动的配置

现在,我已具有正常工作的代码特性化版本,我想说明如何使用 RegistrationBuilder 将这些代码段转换为约定驱动的模型。 让我们先删除所有添加了 MEF 特性的类。 例如,让我们看一下图 4 中的代码段,这是从图 2 中显示的 WeatherUnderground 数据分析服务修改而来的。

图 4 转换为简单 C# 类的 WeatherUnderground 数据分析类

class WeatherUndergroundServiceProvider:IWeatherServiceProvider
{
  private IDataSource _source;
  public WeatherUndergroundServiceProvider(IDataSource source)
  {
    _source = source;
  }
  public string GetWeatherForecast(int zipCode)
  {
    string val = _source.GetData(GetResourcePath(zipCode));
    // Some parsing logic here
    return result;
  }
      ...
}

图 1图 3 中的代码将按与图 4 相同的方式进行更改。

接下来,我使用 RegistrationBuilder 定义某些约定以表示我们使用特性指定的内容。 图 5 显示了执行此操作的代码。

图 5 设置约定

RegistrationBuilder builder = new RegistrationBuilder();
    builder.ForType<WeatherServiceView>()
      .Export()
      .SelectConstructor(cinfos => cinfos[0]);
    builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
      .Export<IWeatherServiceProvider>()
      .SelectConstructor(cinfo => cinfo[0]);
    builder.ForTypesDerivedFrom<IDataSource>()
      .Export<IDataSource>();

每个规则声明有两个不同的部分。 一个部分指定要处理的一个类或一组类;另一部分指定要应用于选定的类、这些类的属性或这些类的构造函数的特性、元数据和共享策略。 因此,您可以看到第 2 行、第 5 行和第 8 行启动我定义的三个规则,每个规则的第一部分指定了将规则的其余部分应用到的类型。 例如,在第 5 行中,我希望为从 IWeatherServiceProvider 派生的所有类型应用一个约定。

现在,让我们看一下这些规则,并将它们重新映射到图 1图 2图 3 中的原始特性化代码。 WeatherFileSource(图 3)是直接作为 IDataSource 导出的。 在图 5 中,第 8 行和第 9 行中的规则指定选择从 IDataSource 派生的所有类型,并将它们作为 IDataSource 约定导出。 在图 2 中,您将看到代码导出类型 IWeatherService­Provider,并在其构造函数中要求导入 IDataSource(这是使用 ImportingConstructor 特性修饰的)。 第 5 行、第 6 行和第 7 行指定了图 5 中的该类型的相应规则。 此处添加的代码段是接受 Func<ConstructorInfo[], ConstructorInfo> 的方法 SelectConstructor。 这为我提供了一种指定构造函数的方法。 您可以指定一个约定,例如,具有最小或最大参数个数的构造函数始终为 ImportingConstructor。 在我的示例中,由于只有一个构造函数,因此,我可以使用选择第一个也是唯一一个构造函数的简单案例。 对于图 1 中的代码,图 5 中的规则是在第 2 行、第 3 行和第 4 行中定义的,并且与刚才讨论的规则类似。

在制订了规则的情况下,我需要将它们应用于应用程序中存在的类型。 为此,所有目录现在都具有一个接受 RegistrationBuilder 作为参数的重载。 因此,您应该修改以前的 CompositionContainer 代码,如图 6 所示。

图 6 使用约定

class Program
{
  static void Main(string[] args)
  {
    // Put the code to build the RegistrationBuilder here
    AssemblyCatalog cat = 
      new AssemblyCatalog(typeof(Program).Assembly,builder);
    CompositionContainer container = new CompositionContainer(cat);           
    WeatherServiceView forecaster =
      container.GetExportedValue<WeatherServiceView>();
    // Accept a ZIP code and call the viewer
    forecaster.GetWeatherForecast(zipCode);
  }
}

集合

现在,我已完成了相应的工作,并且我的简单 MEF 应用程序已在没有特性的情况下启动并运行。 要是生活都是这样简单就好了! 现在,有人告诉我需要能够在应用程序中支持多个气象服务,并且它需要显示所有气象服务提供的预报信息。 所幸的是,由于我使用了 MEF,因此,我并不慌乱。 这只是一个包含接口的多个实现的方案,并且我需要迭代访问这些实现。 现在,我的示例包含多个 IWeatherServiceProvider 实现,并且我希望显示所有这些气象引擎提供的结果。 让我们看一下我需要进行的更改,如图 7 所示。

图 7 启用多个 IWeatherServiceProvider

public class WeatherServiceView
{
  private IEnumerable<IWeatherServiceProvider> _providers;
  public WeatherServiceView(IEnumerable<IWeatherServiceProvider> providers)
  {
    _providers = providers;
  }
  public void GetWeatherForecast(int zipCode)
  {
    foreach (var _provider in _providers)
    {
      Console.WriteLine("Weather Forecast");
      Console.WriteLine(_provider.GetWeatherForecast(zipCode));
    }
    }
}

就是这样了! 我更改了 WeatherServiceView 类以接受一个或多个 IWeatherServiceProvider 实现,并在逻辑部分中迭代访问该集合。 我前面制订的约定现在捕获并导出所有 IWeatherServiceProvider 实现。不过,我的约定中似乎缺少一些内容: 在任何时候,在配置 WeatherServiceView 时,我都不需要添加 ImportMany 特性或等效的约定。 这就是 RegistrationBuilder 的神奇之处,它指明如果您的参数上具有 IEnumerable<T>,则它一定是 ImportMany,而无需明确指定它。 因此,使用 MEF 可将扩展应用程序的操作变得非常简单;并且通过使用 RegistrationBuilder,只要新版本实现了 IWeaterServiceProvider,我就不需要执行任何操作以使其在我的应用程序中使用。 太棒了!

“日期”

MEF 中的另一个非常有用的功能是,能够将元数据添加到部件中。 为了便于讨论,假定在我们讨论的示例中,GetResourcePath 方法返回的值(如图 2 所示)受所使用的 IDataSource 和 IWeatherServiceProvider 的具体类型的约束。 因此,我定义了一个命名约定,以指定将资源命名为由下划线(“_”)分隔的气象服务提供程序和数据源组合。 按照这种约定,具有 Web 数据源的 Weather Underground 服务提供程序将具有名称 WeatherUnderground_Web_ResourceString。 图 8 中显示了此约定的代码。

图 8 资源说明定义

public class ResourceInformation
{
  public string Google_Web_ResourceString
  {
    get { return "http://www.google.com/ig/api?weather="; }
  }
  public string Google_File_ResourceString
  {
    get { return @".
\GoogleWeather.txt"; }
  }
  public string WeatherUnderground_Web_ResourceString
  {
    get { return
      "http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
  }
}

通过使用此命名约定,我现在可以在 WeatherUnderground 和 Google 气象服务提供程序中创建一个属性,以便导入所有这些资源字符串,并根据其当前配置选择相应的字符串。 让我们先看一下如何编写 RegistrationBuilder 规则以将 ResourceInformation 配置为 Export(请参阅图 9)。

图 9 导出属性和添加元数据的规则

builder.ForType<ResourceInformation>()
       .ExportProperties(pinfo => 
       pinfo.Name.Contains("ResourceString"),
    (pinfo, eb) =>
      {
        eb.AsContractName("ResourceInfo");
        string[] arr = pinfo.Name.Split(new char[] { '_' },
          StringSplitOptions.RemoveEmptyEntries);
        eb.AddMetadata("ResourceAffiliation", arr[0]);
        eb.AddMetadata("ResourceLocation", arr[1]);
     });

第 1 行仅指定了类。 第 2 行定义了一个谓词,以选择此类中所有包含 ResourceString 的属性,这正是我的约定规定的内容。 ExportProperties 的最后一个参数是 Action<PropertyInfo,ExportBuilder>,我在其中指定我希望将与第 2 行中指定的谓词匹配的所有属性导出为名为 ResourceInfo 的命名约定,并希望使用键 ResourceAffiliation 和 ResourceLocation 根据该属性名称的分析结果添加元数据。 在使用端,我现在需要将一个属性添加到所有 IWeatherServiceProvider 实现中,如下所示:

public IEnumerable<Lazy<string, IServiceDescription>> WeatherDataSources { get; set; }

然后,添加以下接口以使用强类型元数据:

public interface IServiceDescription
  {
    string ResourceAffiliation { get; }
    string ResourceLocation { get; }   
  }

要了解元数据和强类型元数据的详细信息,您可以阅读帮助教程 (bit.ly/HAOwwW)。

现在,让我们在 RegistrationBuilder 中添加一个规则,以导入所有具有约定名称 ResourceInfo 的部件。 为此,我从图 5(第 5-7 行)中提取现有规则,并添加以下子句:

builder.ForTypesDerivedFrom<IWeatherServiceProvider>()
       .Export<IWeatherServiceProvider>()
       .SelectConstructor(cinfo => cinfo[0]);
       .ImportProperties<string>(pinfo => true,
                                (pinfo, ib) =>
                                 ib.AsContractName("ResourceInfo"))

第 8 行和第 9 行现在指定从 IWeatherServiceProvider 派生的所有类型应将 Import 应用于类型字符串的所有属性,并且应在约定名称 ResourceInfo 上进行导入。 在运行此规则时,以前添加的属性将变为具有名称 ResourceInfo 的所有约定的 Import。 然后,我可以查询枚举以根据元数据筛选出正确的资源字符串。

特性的时代终结了吗?

如果您考虑我讨论的示例,您就会看到我们似乎确实不再需要使用特性了。 现在,可以使用基于约定的模型实现您使用特性化编程模型执行的任何操作。 我提到了一些 RegistrationBuilder 可提供帮助的常见使用案例,Nicholas Blumhardt 发表的有关 RegistrationBuilder 的精彩文章 (bit.ly/tVQA1J) 可以为您提供详细信息。 不过,特性仍然在约定驱动的 MEF 领域中发挥着重要作用。 一个有关约定的重要问题是,只有在遵循约定时,这些约定才是至关重要的。 一旦规则具有例外情况,保持约定的开销可能是非常大的,但特性可以帮助覆盖约定。 让我们假设在 ResourceInformation 类中添加一个新资源,但其名称并不符合约定,如图 10 所示。

图 10 使用特性覆盖约定

public class ResourceInformation
{
  public string Google_Web_ResourceString
  {
    get { return "http://www.google.com/ig/api?weather="; }
  }
  public string Google_File_ResourceString
  {
    get  { return @".
\GoogleWeather.txt"; }
  }
  public string WeatherUnderground_Web_ResourceString
  {
    get { return "http://api.wunderground.com/api/96863c0d67baa805/conditions/q/"; }
  }
  [Export("ResourceInfo")]
  [ExportMetadata("ResourceAffiliation", "WeatherUnderground")]
  [ExportMetadata("ResourceLocation", "File")]
  public string WunderGround_File_ResourceString
  {
    get { return @".
\Wunder.txt"; }
  }
}

图 10 中,您可以看到按照命名规范,约定的第一部分是不正确的。 不过,通过进入并明确添加正确的约定名称和元数据,您可以覆盖或将其添加到 RegistrationBuilder 找到的部件,从而使 MEF 特性成为一个指定 RegistrationBuilder 定义的约定例外情况的有效工具。

无缝开发

在本文中,我介绍了约定驱动的配置,这是 MEF 的一个新功能,它是在 RegistrationBuilder 类中公开的,可以大大简化与 MEF 有关的开发工作。 您可以在 mef.codeplex.com 中找到这些库的测试版。 如果还没有安装 .NET Framework 4.5,您可以访问 CodePlex 网站并下载这些内容。

具有讽刺意味的是,RegistrationBuilder 可以使您的日常开发活动不再那么以 MEF 为中心,您在项目中使用 MEF 是高度无缝的。 一个很好的示例是内置到 MEF 的 Model-View-Controller (MVC) 中的集成包,您可以阅读 BCL 团队博客 (bit.ly/ysWbdL) 以了解相关信息。 简便方法是,您可以将一个包下载到 MVC 应用程序中,这会将您的项目设置为使用 MEF。 经验表明,无论什么代码“正常工作”,在开始遵循指定的约定时,您可以获得在应用程序中使用 MEF 的好处,而无需亲自编写一行 MEF 代码。 您可以在 BCL 团队博客 (bit.ly/ukksfe) 中了解相关信息。

Alok Shriram是 Microsoft .NET Framework 团队的项目经理,负责基类库方面的工作。 在此之前,他曾是 Office Live 团队的开发人员,该团队后来变为 Office 365 团队。 从查珀尔希尔的北卡罗莱纳大学研究生院毕业后,他目前在西雅图工作。 在业余时间,他喜欢与他的妻子 Mangal 一起游览西北部地区的名山大川。 他可能会隐匿在 MEF CodePlex 网站上,挂在 Twitter 上 (twitter.com/alokshriram),偶尔还会在 .NET 博客上发帖子。

衷心感谢以下技术专家对本文的审阅: Glenn BlockNicholas BlumhardtImmo Landwerth