65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Autofac 进行依赖注入

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (116投票s)

2008年4月19日

MIT

15分钟阅读

viewsIcon

644728

downloadIcon

10218

使用 Autofac 依赖注入容器来简化面向对象应用程序的配置。

目录

引言

Autofac 是一个开源的 依赖注入 (DI) 或控制反转 (IoC) 容器,开发于 Google Code。

Autofac 与 许多相关技术 的不同之处在于,它尽可能地贴近底层 C# 编程。其设计理念是,在 C# 这样强大的语言中工作,却因为像其他 .NET 容器普遍使用的纯反射 API 而失去这种强大能力,这是浪费。

其结果是 Autofac 用很少的附加基础设施或集成代码,以及更低的学习曲线,支持广泛的应用程序设计。

但这并非意味着它很简单;它拥有其他 DI 容器的大部分功能,以及许多微妙的功能,这些功能有助于应用程序的配置、组件生命周期的管理以及对依赖项的控制。

本文使用一个示例演示了创建和连接应用程序组件的最基本技术,然后讨论了 Autofac 最重要的功能。

如果您已经在使用的 DI 容器,并想了解 Autofac 的不同之处,您可以快速跳到 应用程序中的 Autofac 部分,查看其中的代码。

示例应用程序

该应用程序是一个控制台程序,它检查备忘录列表,每个备忘录都有一个截止日期,并通知用户哪些备忘录已逾期。

Memo Checker Console Output

在示例代码中做了许多简化。为了简洁起见,所有代码示例都省略了 XML 注释、参数检查和异常处理。

检查逾期备忘录

应用程序的核心是 MemoChecker 组件,它看起来像这样

// A MemoChecker ... 
class MemoChecker
{
    IQueryable<Memo> _memos;
    IMemoDueNotifier _notifier;
 
    // Construct a memo checker with the store of memos and the notifier 
    // that will be used to display overdue memos. 
    public MemoChecker(IQueryable<Memo> memos, IMemoDueNotifier notifier)
    {
        _memos = memos;
        _notifier = notifier;
    }
 
    // Check for overdue memos and alert the notifier of any that are found. 
    public void CheckNow()
    {
        var overdueMemos = _memos.Where(memo => memo.DueAt < DateTime.Now);
 
        foreach (var memo in overdueMemos)
            _notifier.MemoIsDue(memo);
    }
}

该类有以下几个方面是使用依赖注入风格的直接结果

  • 它接受所有依赖项作为参数,在本例中是通过构造函数
  • 它独立于持久化 - IQueryable 存储备忘录的可能是一个数据库表、一个结构化文件或一个内存集合
  • 它独立于用户通知方式 - 通知者可以发送电子邮件、写入事件日志或打印到控制台

这些使得该类更易于测试、配置和维护。

通知用户

IMemoDueNotifier 接口有一个名为 MemoIsDue() 的方法,该方法由另一个名为 PrintingNotifier 的依赖注入组件实现

// A memo notifier that prints messages to a text stream. 
class PrintingNotifier : IMemoDueNotifier
{
    TextWriter _writer;
 
    // Construct the notifier with the stream onto which it will 
    // print notifications. 
    public PrintingNotifier(TextWriter writer)
    {
        _writer = writer;
    }
 
    // Print the details of an overdue memo onto the text stream. 
    public void MemoIsDue(Memo memo)
    {
        _writer.WriteLine("Memo '{0}' is due!", memo.Title);
    }
}

MemoChecker 类似,该类通过构造函数接受其依赖项。TextWriter 类型是 .NET Framework 中广泛使用的标准 .NET 类,它是 StringWriterSystem.Console.Out 等类的基类。

数据存储

MemoCheckerIQueryable<Memo> 获取逾期备忘录。示例中使用的数据存储是一个内存列表

IQueryable<Memo> memos = new List<Memo>() {
    new Memo { Title = "Release Autofac 1.0", DueAt = new DateTime(2007, 12, 14) },
    new Memo { Title = "Write CodeProject Article", DueAt = DateTime.Now },
    new Memo { Title = "Release Autofac 2.3", DueAt = new DateTime(2010, 07, 01) }
}.AsQueryable();

自 .NET 3.5 引入的 IQueryable 接口适用于作为 Memo 的来源,因为它既可以查询内存对象,也可以查询关系数据库中的行。

组件连接

应用程序的最终结构如下

Memo Checker Class Diagram

本文大部分内容都涉及 MemoChecker 如何与它的通知器和备忘录服务关联,以及这些对象各自如何被 '连接' 到它们自己的依赖项。

手动依赖注入

只有几个组件时,手动配置 MemoChecker 并不难。事实上,这是微不足道的,看起来是这样的

var checker = new MemoChecker(memos, new PrintingNotifier(Console.Out));
checker.CheckNow();

一个真实的应用程序,拥有多个层和各种组件,不应该这样配置。这种直接的对象创建方式在少量类上本地工作得很好,但无法扩展到大量的组件。

首先,负责此事的启动代码将随着时间的推移而变得越来越复杂,并且可能需要每次类依赖项发生变化时进行重新组织。

更重要的是,很难切换服务的实现;例如,可以将 EmailNotifier 替换为打印通知器,但它本身将具有可能与 PrintingNotifier 不同的依赖项,但可能与其它组件的依赖项相交。(这是可组合性问题,值得单独写一篇文章。)

Autofac 和其他依赖注入容器通过在配置时 '展平' 对象图的深层嵌套结构来规避这些问题...

使用容器进行依赖注入

在使用 Autofac 时,访问 MemoChecker 与创建它是分开的

container.Resolve<MemoChecker>().CheckNow();

container.Resolve() 调用请求一个 MemoChecker 实例,该实例已设置完毕并准备就绪。那么,容器是如何确定如何创建 MemoChecker 的呢?

组件注册

依赖注入容器是服务到组件的注册集合。在此上下文中,服务是识别特定功能能力的某种方式 - 它可以是文本名称,但更常见的是接口类型。

注册捕获了组件在系统中的动态行为。其中最引人注目的是组件实例的创建方式。

Autofac 可以接受使用表达式、提供实例或基于 System.Type 的反射来创建组件的注册。

注册通过表达式创建的组件

以下设置了 MemoChecker 组件的注册

builder.Register(c => new MemoChecker(c.Resolve<IQueryable<Memo>>(), 
                                      c.Resolve<IMemoDueNotifier>()));

每个 Register() 语句只处理最终对象图的一部分及其与直接依赖项的关系。

提供给 Register() 的 lambda 表达式 c => new MemoChecker(...) 将由容器用于创建 MemoChecker 组件。

每个 MemoChecker 都依赖于两个额外的服务:IQueryable<Memo>IMemoDueNotifier。这些服务在 lambda 表达式内部通过调用传递给参数 c 的容器的 Resolve() 方法来检索。

注册没有说明哪个组件将实现 IQueryable<Memo>IMemoDueNotifier - 这两个服务是独立配置的,方式与 MemoChecker 相同。

提供给 Register() 的表达式的返回类型是 MemoChecker,因此 Autofac 将使用它作为此注册的默认服务,除非使用 As() 方法指定了另一个服务。为了更清晰,可以包含 As() 调用

builder.Register(c => new MemoChecker(...)).As<MemoChecker>();

无论哪种方式,从容器请求 MemoChecker 都将导致调用我们的表达式。

Autofac 不会在组件注册时执行表达式。相反,它会等到调用 Resolve<MemoChecker>() 时才执行。这一点很重要,因为它消除了组件注册顺序的一个依赖点。

注册组件实例

IQueryable<Memo> 服务由现有的 memos 实例提供,而 PrintingMemoNotifier 类最终与 TextWriter 实例 Console.Out 连接

builder.RegisterInstance(memos);
builder.RegisterInstance(Console.Out).As<TextWriter>().ExternallyOwned();

memosConsole.Out 作为已创建的实例提供给容器。(关于 ExternallyOwned() 的解释,请参见 确定性释放。)

注册具有实现类型的组件

Autofac 也可以像其他容器那样使用反射来创建组件(许多容器通过生成 MSIL 来优化这种情况)。

这意味着您可以告诉 Autofac 提供服务的类型,它将找出如何调用最合适的构造函数,并根据其他可用服务选择参数。

MemoChecker 的注册可以替换为

builder.RegisterType<MemoChecker>();

通常,自动装配最常见的用途是注册一批组件,例如

foreach (Type t in Assembly.GetExecutingAssembly().GetTypes())
    if (typeof(IController).IsAssignableFrom(t))
        builder.Register(t);

这使得大量组件可用,而无需注册每个组件的开销,在这种情况下您绝对应该考虑它。Autofac 提供了快捷方式来以这种方式注册组件批次

builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
    .As<IController>();

自动装配在通过应用程序的 XML 配置文件 注册组件时也非常有用。

完成示例

在从容器请求 MemoChecker 服务之前创建组件注册的过程如下图所示

var builder = new ContainerBuilder();
builder.Register(c => new 
  MemoChecker(c.Resolve<IQueryable<Memo>>(), 
              c.Resolve<IMemoDueNotifier>()));
builder.Register(c => new 
  PrintingNotifier(c.Resolve<TextWriter>())).As<IMemoDueNotifier>();
builder.RegisterInstance(memos);
builder.RegisterInstance(Console.Out).As<TextWriter>().ExternallyOwned();
 
using (var container = builder.Build())
{
    container.Resolve<MemoChecker>().CheckNow();
}

配置代码中缺乏嵌套,表明了容器提供的依赖结构 '展平'。

可能很难看出这如何比 '手动' 示例中的直接对象构造更简单,但请再次记住,这个示例应用程序的组件数量远远少于大多数有用系统。

需要注意的最重要区别是 **每个组件现在都独立于所有其他组件进行配置。** 随着更多组件添加到系统中,它们可以纯粹根据它们公开的服务和它们所需的服务来理解。这是控制架构复杂性的一种有效方法。

确定性释放

IDisposable 既是恩赐也是诅咒。它提供了一种沟通组件应被清理的统一方式,这很好。不幸的是,哪个组件应该进行清理,以及何时进行清理,并不总是容易确定。

允许多个服务实现的设计使问题变得更糟。在示例应用程序中,可能部署许多不同的 IMemoDueNotifier 实现。其中一些将在工厂中创建,一些将是单例,一些需要释放,一些则不需要。

使用通知器的组件无法知道它们是否应该尝试将其转换为 IDisposable 并调用 Dispose()。由此产生的记账工作既容易出错又繁琐。

Autofac 通过跟踪容器创建的所有可释放对象来解决此问题。注意上面提供的示例

using (var container = builder.Build())
{
    container.Resolve<MemoChecker>().CheckNow();
}

容器位于 using 块中,因为它拥有它创建的所有组件的所有权,并在自身被释放时释放它们。

这一点很重要,因为为了实现将使用与配置分离的精神,MemoChecker 服务可以在任何需要的地方使用 - 即使是作为另一个组件的依赖项间接创建 - 而无需担心它是否应该被清理。

有了这个,就可以安心了 - 您甚至不需要回头阅读示例来发现其中是否有任何类实现了 IDisposable(它们没有),因为您可以信赖容器能够做好正确的事情。

禁用释放

请注意上面完整配置示例中 Console.Out 注册中添加的 ExternallyOwned() 子句。这是可取的,因为 Console.Out 是可释放的,但容器不应该释放它。

对组件生命周期的细粒度控制

容器通常在应用程序执行期间存在,释放它是一种释放具有相同应用程序生命周期的组件所持有的资源的好方法。大多数非简单的程序还应在其他时间释放资源:在 HTTP 请求完成时,在工作线程退出时,或在用户会话结束时。

Autofac 通过嵌套的生命周期范围帮助您管理这些生命周期

using (var appContainer = builder.Build())
{
  using (var request1Lifetime = appContainer.BeginLifetimeScope())
  {
    request1Lifetime.Resolve<MyRequestHandler>().Process();
    // resources associated with request 1 are freed 
  }
 
  using (var request2Lifetime = appContainer.BeginLifetimeScope())
  {
    request2Lifetime.Resolve<MyRequestHandler>().Process();
    // resources associated with request 2 are freed 
  }
 
  // resources at the application level are freed 
}

通过配置组件实例如何映射到生命周期范围来实现生命周期管理。

组件生命周期

Autofac 允许您指定一个组件可以存在多少实例以及它们如何在其他组件之间共享。

独立于其定义来控制组件的范围,这比通过静态 Instance 属性定义单例等传统方法有了非常重要的改进。这是因为对象 *是什么* 与它 *如何使用* 之间存在区别。

Autofac 中最常用的生命周期设置是

  • 单例实例
  • 每次依赖项的实例
  • 每次生命周期范围的实例

单例实例

对于单例生命周期,容器中最多会有一个组件实例,并且会在注册该组件的容器被释放时释放(例如,上面的 appContainer)。

可以使用 SingleInstance() 修改器将组件配置为具有此生命周期

builder.Register(c => new MyClass()).SingleInstance();

每次从容器请求此类组件时,都会返回相同的实例

var a = container.Resolve<MyClass>();
var b = container.Resolve<MyClass>();
Assert.AreSame(a, b);

每次依赖项的实例

当组件注册中没有指定生命周期设置时,默认假定为每次依赖项的实例。每次从容器请求此类组件时,都会创建一个新实例

var a = container.Resolve<MyClass>();
var b = container.Resolve<MyClass>();
Assert.AreNotSame(a, b);

以这种方式解析的组件将与请求它的生命周期范围一起被释放。例如,如果构造一个单例组件需要一个每次依赖项的组件,那么这个每次依赖项的组件将与单例组件一起在容器的生命周期内存在。

每次生命周期范围的实例

最后一个基本生命周期模型是每次生命周期范围,使用 InstancePerLifetimeScope() 修改器实现

builder.Register(c => new MyClass()).InstancePerLifetimeScope();

这提供了实现每次线程、每次请求或每次事务组件生命周期所需的灵活性。只需创建一个持续所需生命周期作用域的生命周期范围。同一范围对象的请求将检索相同的实例,而不同范围中的请求将导致不同的实例

var a = container.Resolve<MyClass>();
var b = container.Resolve<MyClass>();
Assert.AreSame(a, b);
 
var inner = container.BeginLifetimeScope();
var c = inner.Resolve<MyClass>();
Assert.AreNotSame(a, c);

使用范围控制可见性

组件的依赖项只能由同一范围或外部(父)范围内的其他组件满足。这确保了组件的依赖项不会比它先被释放。如果需要应用程序、会话和请求的嵌套,那么容器将被创建为

var appContainer = builder.Build();
var sessionLifetime = appContainer.BeginLifetimeScope();
var requestLifetime = sessionLifetime.BeginLifetimeScope();
var controller = requestLifetime.Resolve<IController>("home");

请注意,在这些示例中,appContainer 将有许多由它创建的 sessionLifetime 子项(每个会话一个),并且每个会话在其生命周期内将是许多 requestLifetime 的父项(每个会话一个 HTTP 请求)。

在这种场景下,允许的依赖方向是请求 -> 会话 -> 应用程序。在处理用户请求中使用的组件可以引用任何其他组件,但反向依赖是不允许的,因此,例如,一个应用程序级别的单例组件不会连接到一个特定于单个用户会话的组件。

在这种层次结构中,Autofac 将始终在最短生命周期的生命周期内响应组件请求。这通常是请求生命周期。单例组件自然会驻留在应用程序级别。要将组件的生命周期固定到会话级别,请参阅 Autofac Wiki 上的 标签文章

Autofac 的范围模型灵活且强大。范围与嵌套生命周期释放之间的关系使得大量的依赖项配置成为可能,同时强制一个对象至少与依赖它的对象一样长寿。

应用程序中的 Autofac

依赖注入是一种极其强大的结构机制,但要获得这些优势,系统的大部分组件都需要通过容器提供给其他组件。

通常,这会带来一些挑战。在现实世界中,现有的组件、框架和架构通常都有自己独特的 '创建' 或生命周期要求。

到目前为止描述的 Autofac 功能旨在将现有的 '纯 .NET' 组件集成到容器中,而无需进行修改或适配器代码。

表达性注册

使用表达式进行组件注册使将 Autofac 集成到应用程序中变得轻而易举。一些示例场景说明了 Autofac 所促进的事情

现有的工厂方法可以使用表达式暴露

builder.Register(c => MyFactory.CreateProduct()).As<IProduct>();

需要首次访问的现有单例可以使用表达式注册,并且加载将保持 '延迟'

builder.RegisterInstance(c => MySingleton.Instance);

参数可以从任何可用来源传递给组件

builder.RegisterInstance(c => new MyComponent(Settings.SomeSetting));

甚至可以根据参数选择实现类型

builder.Register<CreditCard>((c, p) => {
    var accountId = p.Get<string>("accountId");
    if (accountId.StartsWith("9"))
      return new GoldCard(accountId);
    else 
      return new StandardCard(accountId);
  });

简化集成

在此上下文中,集成意味着使现有库和应用程序组件的服务可通过容器获得。

Autofac 支持一些典型的集成场景,例如在 ASP.NET 应用程序中使用;但是,Autofac 模型的灵活性使得许多集成任务变得如此简单,以至于最好由设计者根据其应用程序最适合的方式来实现。

基于表达式的注册和确定性释放,加上组件解析的 '延迟性',在集成技术时可能会出奇地有用

var builder = new ContainerBuilder();
 
builder.Register(c => new ChannelFactory<ITrackListing>(new BasicHttpBinding(), 
    new EndpointAddress("https:///Tracks")))
  .As<IChannelFactory<ITrackListing>>();
 
builder.Register(c => c.Resolve<IChannelFactory<ITrackListing>>().CreateChannel())
  .As<ITrackListing>()
  .UseWcfSafeRelease();
 
using (var container = builder.Build())
{
  var trackService = container.Resolve<ITrackListing>();
  var tracks = trackService.GetTracks("The Shins", "Wincing the Night Away");
  ListTracks(tracks);
}

这是来自 Autofac 网站的 WCF 客户端集成 示例。这里的两个关键服务是 ITrackListingIChannelFactory<ITrackListing> - 这些常见的 WCF 管道组件很容易集成到基于表达式的注册中。

这里有一些注意事项

  1. 通道工厂不会被创建,除非需要它,但一旦创建,它将被保留并在每次请求 ITrackListing 时重用。
  2. ITrackListing 不继承自 IDisposable,但在 WCF 中,以这种方式创建的客户端服务代理需要转换为 IDisposable 并释放。使用 ITrackListing 的代码可以对此实现细节保持不知情。
  3. 终结点信息可以来自任何地方 - 另一个服务、数据库、配置文件(通过 WCF 的其他预建容器集成,这些决定已为您做出)。
  4. 除基本的 Register() 方法外,不使用任何其他概念(要在任何其他容器中做到这一点,都需要实现自定义类/设施)。

本节应让您对使用 Autofac 如何让您专注于编写应用程序而不是扩展或纠结于 DI 容器的复杂性有所了解。

下一步?

我希望本文能说明学习如何使用 Autofac 将带来的回报。下一步可能是

致谢

感谢 Rinat AbdullinLuke Marshall、Tim Mead 和 Mark Monsour 审阅本文并提出许多有益的建议。没有你们的帮助,我担心它将完全无法理解。如果仍然无法理解,那么所有的责任都在我身上。:)

历史

  • 2008年4月18日:首次发布
  • 2010年9月9日:文章更新
© . All rights reserved.