Autofac 隐式关系类型
Autofac 支持自动解决特定类型,以支持组件和服务之间的特殊关系。要利用这些关系,请正常注册组件,但更改消耗组件的构造函数参数或在 Resolve()
调用中指定的关系类型。
例如,当 Autofac 注入一个 IEnumerable<ITask>
类型的构造函数参数时,它不会 寻找提供 IEnumerable<ITask>
的组件。相反,容器将找到所有实现 ITask
的组件,并注入它们。
(不用担心,下面有例子展示了各种类型的使用方法及其含义。)
注意:为了覆盖默认行为,仍然可以显式注册这些类型的实现。
[本文档的内容基于 Nick Blumhardt 的博客文章 关系动物园]
支持的关系类型
下表总结了 Autofac 中支持的各种关系类型,并显示了您可以使用的 .NET 类型来消费它们。每个关系类型后面都有更详细的描述和用例。
关系 | 类型 | 意义 |
---|---|---|
A 需要 B | B |
直接依赖 |
A 在未来某个时间需要 B | Lazy<B> |
延迟实例化 |
A 在未来某个时间需要 B | Owned<B> |
受控生命周期 |
A 需要创建 B 的实例 | Func<B> |
动态实例化 |
A 需要为 B 提供参数 X 和 Y | Func<X, Y, B> |
参数化实例化 |
A 需要所有种类的 B | IEnumerable<B> 、IList<B> 、ICollection<B> |
列表 |
A 需要知道关于 B 的 X | Meta<B> 和 Meta<B, X> |
元数据查询 |
A 根据 X 选择 B | IIndex<X, B> |
键控服务 查找 |
直接依赖 (B)
直接依赖 是最基础的关系,组件 A 需要服务 B。这通过标准构造函数和属性注入自动处理:
public class A
{
public A(B dependency) { ... }
}
注册 A
和 B
组件,然后解耦:
var builder = new ContainerBuilder();
builder.RegisterType<A>();
builder.RegisterType<B>();
var container = builder.Build();
using (var scope = container.BeginLifetimeScope())
{
// B 自动注入到 A。
var a = scope.Resolve<A>();
}
延迟实例化 (Lazy<B>)
延迟依赖 不会在首次使用时实例化。这种关系出现在依赖很少使用或者创建成本较高的情况下。要利用这一点,请在 A
的构造函数中使用 Lazy<B>
:
public class A
{
Lazy<B> _b;
public A(Lazy<B> b) { _b = b }
public void M()
{
// 当调用 M() 时,实现 B 的组件被创建
_b.Value.DoSomething();
}
}
如果您有一个需要元数据的延迟依赖,可以使用 Lazy<B, M>
而不是较长的 Meta<Lazy<B>, M>
。
受控生命周期 (Owned<B>)
受控依赖 可以在不再需要时由拥有者释放。受控依赖通常对应于依赖组件执行的一些工作单元。
这种关系类型尤其有趣,当与实现 IDisposable
的组件一起工作时。Autofac 在生命周期范围结束时自动丢弃可丢弃的组件 ,但这可能意味着组件被持有过久;或者您可能只想自己控制对象的释放。在这种情况下,您可以使用一个受控依赖。
public class A
{
Owned<B> _b;
public A(Owned<B> b) { _b = b; }
public void M()
{
// _b 用于某些任务
_b.Value.DoSomething();
// 在这里,_b 已经不再需要,所以释放它
_b.Dispose();
}
}
内部,Autofac 在一个小型生命周期范围内为 B
服务进行解析。当您调用 Dispose()
时,生命周期范围会被丢弃。这意味着如果依赖项不是共享的(例如单例),那么丢弃 B
也将同时丢弃它的依赖项。
这也意味着,如果您在一个生命周期范围内注册了 InstancePerLifetimeScope()
,并且将其作为 Owned<B>
解决,那么您可能不会在同一个生命周期范围内获得与其他地方相同的实例。这个示例显示了问题:
var builder = new ContainerBuilder();
builder.RegisterType<A>().InstancePerLifetimeScope();
builder.RegisterType<B>().InstancePerLifetimeScope();
var container = builder.Build();
using (var scope = container.BeginLifetimeScope())
{
// 这里我们从生命周期范围内解析了一个 B,它是 InstancePerLifetimeScope();
var b1 = scope.Resolve<B>();
b1.DoSomething();
// 这将是上面的 b1。
var b2 = scope.Resolve<B>();
b2.DoSomething();
// A 中使用的 B 并不与其他人相同。
var a = scope.Resolve<A>();
a.M();
}
这是设计上的原因,因为您不希望一个组件在其他所有组件之下丢弃 B
。然而,如果没有意识到,这可能会导致一些混淆。
如果您始终想自行控制 B
的释放,可以将 B 注册为 ExternallyOwned() 。
动态实例化 (Func<B>)
使用自动生成的工厂 可以让您在程序控制流内动态地在程序中解决 B
,而无需直接依赖 Autofac 库。如果满足以下条件,请使用此关系类型:
- 需要创建特定服务的多个实例。
- 想要特别控制服务设置的时间。
- 对是否需要服务不确定,希望在运行时决定。
这种关系在 WCF 集成 等场景中也很有用,其中需要在通道故障后创建新的服务代理。
Func<B>
行为就像调用 Resolve<B>()
。这意味着它不仅限于处理无参构造函数的对象,它会连接构造参数、执行属性注入,并遵循 Resolve<B>()
所做的整个生命周期。
此外,生命周期范围受到尊重。如果您将对象注册为 InstancePerDependency()
,并多次调用 Func<B>
,每次都会得到一个新的实例;如果您将对象注册为 SingleInstance()
,并且多次调用 Func<B>
来解决对象,无论您传递多少次,都将始终获得同一个对象实例。
这种关系的一个示例如下:
public class B
{
public B() {}
public void DoSomething() {}
}
public class A
{
Func<B> _bFactory;
public A(Func<B> b) { _bFactory = b; }
public void M()
{
// 调用 Func<B> 从生命周期范围中解析它。这就像调用 Resolve<B>() - 如果有任何构造参数,它们都会从范围中解析。
var b = _bFactory();
b.DoSomething();
}
}
注册 A
和 B
组件,然后解耦:
var builder = new ContainerBuilder();
builder.RegisterType<A>();
builder.RegisterType<B>();
var container = builder.Build();
using (var scope = container.BeginLifetimeScope())
{
// B 实际上直到 A 调用注入的 Func<B> 工厂方法才会从范围中解析。
var a = scope.Resolve<A>();
// 这里会解析一个 B!
a.M();
// 由于 B 注册为 InstancePerDependency,每次调用 a.M() 都会解析一个新的 B 实例。
a.M();
a.M();
}
生命周期范围受到尊重,因此您可以利用这一点。
// 这次 B 将注册为 InstancePerLifetimeScope,所以在给定范围内的所有解析调用都将获得同一个 B 实例。
var builder = new ContainerBuilder();
builder.RegisterType<A>();
builder.RegisterType<B>().InstancePerLifetimeScope();
var container = builder.Build();
using (var scope = container.BeginLifetimeScope())
{
// B 在 A.M() 方法内部解析,但由于 B 注册为 InstancePerLifetimeScope,所以每次调用 A.M() 方法都会获得同一个实例。
var a = scope.Resolve<A>();
a.M();
a.M();
a.M();
}
参数化实例化 (Func<X, Y, B>)
你可以使用自动生成的工厂来创建对象实例时提供参数,此时对象的构造函数需要额外的一些参数。虽然 Func<B>
关系类似于 Resolve<B>()
,但 Func<X, Y, B>
关系则像是调用 Resolve<B>(TypedParameter.From<X>(x), TypedParameter.From<Y>(y))
——一个具有类型参数的解析操作。这提供了与 在注册时传递参数 或手动 解析时传递参数 时不同的选择:
public class B
{
public B(string someString, int id) {}
public void DoSomething() {}
}
public class A
{
// 这里的参数类型与 B 构造函数中的类型匹配
Func<int, string, B> _bFactory;
public A(Func<int, string, B> b) { _bFactory = b }
public void M()
{
var b = _bFactory(42, "http://hell.owor.ld");
b.DoSomething();
}
}
请注意,由于我们正在解析实例而不是直接调用构造函数,所以无需按照构造函数定义中参数出现的顺序声明它们,也不需要提供构造函数中列出的所有参数。如果构造函数的一部分参数可以由作用域解决,则可以从 Func<X, Y, B>
的签名中省略这些参数。你只需要列出作用域无法解决的类型即可。
另一种方法是,如果你已经有了一个具体的实例,可以使用此方法覆盖构造函数参数,即使这个参数原本是要从容器中自动解析的。
示例:
// 假设 Q 和 R 已经注册到 Autofac 容器中,但 int 和 P 没有。你需要在运行时提供这些参数
public class B
{
public B(int id, P peaDependency, Q queueDependency, R ourDependency) {}
public void DoSomething() {}
}
public class A
{
// 注意这里没有包含 Q 和 R
Func<int, P, B> _bFactory;
public A(Func<int, P, B> bFactory) { _bFactory = bFactory }
public void M(P existingPea)
{
// Q 和 R 将由 Autofac 解析,但 int 和 P 在这里作为参数提供给你
var b = _bFactory(42, existingPea);
b.DoSomething();
}
}
Autofac 根据类型(就像 TypedParameter
)确定构造参数的值。由此产生的后果是,**自动生成的函数工厂不能在输入参数列表中有重复的类型。**例如,假设你有一个这样的类型:
public class DuplicateTypes
{
public DuplicateTypes(int a, int b, string c)
{
// ...
}
}
你可能想要注册该类型,并为其提供一个自动生成的函数工厂。你可以成功地解析函数,但无法执行它。你可以尝试使用每个类型的其中一个参数来解析一个工厂,这将正常工作,但所有相同类型的构造参数都将使用相同的输入值。
// 这个自动生成的工厂有两个相同的类型参数——问题来了!
var funcWithDuplicates = scope.Resolve<Func<int, int, string, DuplicateTypes>>();
// 因为两个 int 类型,抛出依赖解析异常
var obj1 = funcWithDuplicates(1, 2, "three");
// 这个自动生成的工厂移除了重复项,但是……
var funcWithoutDuplicates = container.Resolve<Func<int, string, DuplicateTypes>>();
// ……对于两个 int 工厂参数,将使用相同的值。这个工厂调用将有效,但就像这样
// var obj2 = new DuplicateTypes(1, 1, "three");
var obj2 = funcWithoutDuplicates(1, "three");
如果你确实需要多个相同类型的参数,可以查看 委托工厂 。
作用域的生命周期受到这种关系类型的尊重,就像使用 Func<B>
或 委托工厂 时一样。 如果你将对象注册为 InstancePerDependency()
,并且多次调用 Func<X, Y, B>
,每次都会得到一个新的实例。然而,如果你将对象注册为 SingleInstance()
,并且多次调用 Func<X, Y, B>
来解析对象,无论你传递不同的参数,你都会得到同一个对象实例。仅仅传递不同的参数不会打破对作用域生命周期的尊重:
var builder = new ContainerBuilder();
builder.RegisterType<A>();
builder.RegisterType<B>();
builder.RegisterType<Q>();
builder.RegisterType<R>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope())
{
// B 实际上直到 A 调用注入的 Func<int, P, B> 工厂方法时才会从作用域中解析
var a = scope.Resolve<A>();
var p = new P();
// 这里会解析一个 B!
a.M(p);
// 因为 B 注册为 InstancePerDependency,每次调用 a.M(P) 都会解析一个新的 B 实例
a.M(p);
a.M(p);
}
这展示了无论参数如何,作用域的生命周期是如何受到尊重的:
// 注意这次我们将 B 注册为 InstancePerLifetimeScope,这意味着每次作用域中都会只解析一次 B,即使传递了不同的参数
var builder = new ContainerBuilder();
builder.RegisterType<B>().InstancePerLifetimeScope();
builder.RegisterType<Q>();
builder.RegisterType<R>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope())
{
// 获取工厂,就像在 A 类中一样
var factory = scope.Resolve<Func<int, P, B>>();
// 第一次调用,B 将使用这些参数进行解析
var b1 = factory(10, new P());
// 第二次调用,尽管参数更改了,但由于 B 注册为 InstancePerLifetimeScope,B 仍将是同一个实例
var b2 = factory(17, new P());
// 在单元测试中,这将通过,因为它们是同一个实例
Assert.Same(b1, b2);
}
委托工厂允许你为工厂函数提供自定义委托签名,以克服像 Func<X, Y, B>
这样的关系带来的挑战,比如支持多个相同类型的参数。 委托工厂可能是生成工厂的强大替代方案——请参阅 高级主题部分 中的这一特性。
序列(IEnumerable<B>, IList<B>, ICollection<B>)
依赖于 可枚举类型 提供同一服务(接口)的多个实现。这对于消息处理器等场景很有帮助,其中一条消息进来,注册了多个处理器来处理消息。
假设你有一个依赖接口定义如下:
public interface IMessageHandler
{
void HandleMessage(Message m);
}
此外,你有一个消费者依赖,它需要注册多个并接收所有注册的依赖:
public class MessageProcessor
{
private IEnumerable<IMessageHandler> _handlers;
public MessageProcessor(IEnumerable<IMessageHandler> handlers)
{
this._handlers = handlers;
}
public void ProcessMessage(Message m)
{
foreach(var handler in this._handlers)
{
handler.HandleMessage(m);
}
}
}
你可以轻松地使用隐式序列关系类型来实现这一点。只需注册所有依赖和消费者,当你解析消费者时,所有匹配的依赖项集将被解析为枚举。
var builder = new ContainerBuilder();
builder.RegisterType<FirstHandler>().As<IMessageHandler>();
builder.RegisterType<SecondHandler>().As<IMessageHandler>();
builder.RegisterType<ThirdHandler>().As<IMessageHandler>();
builder.RegisterType<MessageProcessor>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope())
{
// 当 processor 被解析时,它将获得所有已注册的处理器作为构造函数参数
var processor = scope.Resolve<MessageProcessor>();
processor.ProcessMessage(m);
}
如果容器中没有注册任何匹配项,序列支持将返回一个空集合。 也就是说,使用上面的例子,如果没有注册任何 IMessageHandler
实现,这将会失败:
// 抛出异常,因为没有注册任何
scope.Resolve<IMessageHandler>();
但这是可行的:
// 返回一个空列表,而不是异常
scope.Resolve<IEnumerable<IMessageHandler>>();
这可能会造成一些“陷阱”,你可能会认为如果使用这种关系注入某物,你会得到 null。实际上,你会得到一个空列表。
元数据查询(Meta<B>, Meta<B, X>)
Autofac 的 元数据功能 允许您将任意数据与服务关联起来,在解决依赖时可以使用这些数据来做出决策。如果您希望在消费组件中做出这些决策,可以使用 Meta<B>
关系,它会为您提供一个包含所有对象元数据的字符串/对象字典:
public class A
{
Meta<B> _b;
public A(Meta<B> b) { _b = b; }
public void M()
{
if (_b.Metadata["SomeValue"] == "yes")
{
_b.Value.DoSomething();
}
}
}
您还可以使用 强类型元数据 ,通过指定 Meta<B, X>
关系中的元数据类型:
public class A
{
Meta<B, BMetadata> _b;
public A(Meta<B, BMetadata> b) { _b = b; }
public void M()
{
if (_b.Metadata.SomeValue == "yes")
{
_b.Value.DoSomething();
}
}
}
如果您需要为懒加载依赖项获取元数据,可以使用 Lazy<B, M>
而不是更长的 Meta<Lazy<B>, M>
。
键控服务查找(IIndex<X, B>)
当您有很多相同类型的服务(如 IEnumerable<B>
关系),但希望根据 服务键 选择一个服务时,可以使用 IIndex<X, B>
关系。首先,使用键注册您的服务:
var builder = new ContainerBuilder();
builder.RegisterType<DerivedB>().Keyed<B>("first");
builder.RegisterType<AnotherDerivedB>().Keyed<B>("second");
builder.RegisterType<A>();
var container = builder.Build();
然后使用 IIndex<X, B>
获取带有键的服务字典:
public class A
{
IIndex<string, B> _b;
public A(IIndex<string, B> b) { _b = b; }
public void M()
{
var b = this._b["first"];
b.DoSomething();
}
}
组合关系类型
关系类型可以组合,因此:
IEnumerable<Func<Owned<ITask>>>
会被正确解释为:
- 所有实现
- 工厂,返回
- 所有实例
ITask
服务
关系类型与容器独立性
基于标准 .NET 类型的 Autofac 自定义关系类型不会强制您的应用程序更紧密地绑定到 Autofac。它们为您提供了一种与编写其他组件一致的容器配置编程模型(而不是必须了解很多特定的容器扩展点和 API,这些可能还会集中您的配置)。
例如,您仍然可以在核心模型中创建自定义 ITaskFactory
,但如果需要,可以提供一个基于 Func<Owned<ITask>>
的 AutofacTaskFactory
实现。
请注意,有些关系是基于 Autofac 中的类型(如 IIndex<X, B>
)。使用这些关系类型确实会将您绑定到至少引用 Autofac,即使您选择使用不同的 IoC 容器来实际解析服务。