Autofac 注册概念
通过创建一个 ContainerBuilder
并告知构建器哪些 [组件] ../Glossary.md) 暴露哪些 服务 ,你可以注册 Autofac
中的 组件。
组件可以通过反射(注册特定的.NET 类型或泛型开放类型)、提供现成的实例(你创建的对象的实例)或使用lambda 表达式(执行以实例化对象的匿名函数)来创建。ContainerBuilder
提供了一系列 Register()
方法,用于设置这些配置。
每个组件都会暴露一个或多个服务,这些服务通过 ContainerBuilder
上的 As()
方法进行绑定。
// 创建用于注册组件和服务的构建器。
var builder = new ContainerBuilder();
// 注册暴露接口的类型...
builder.RegisterType<ConsoleLogger>().As<ILogger>();
// 注册你创建的对象实例...
var output = new StringWriter();
builder.RegisterInstance(output).As<TextWriter>();
// 注册执行以创建对象的表达式...
builder.Register(c => new ConfigReader("mysection")).As<IConfigReader>();
// 构建容器以完成注册并准备对象解析。
var container = builder.Build();
// 现在你可以使用Autofac解析服务。例如,这行代码将执行注册到IConfigReader服务的lambda表达式。
using(var scope = container.BeginLifetimeScope())
{
var reader = scope.Resolve<IConfigReader>();
}
反射组件
通过类型注册
反射生成的组件通常通过类型注册:
var builder = new ContainerBuilder();
builder.RegisterType<ConsoleLogger>();
builder.RegisterType(typeof(ConfigReader));
在使用基于反射的组件时,Autofac 会自动使用具有最多可从容器获取参数的类的构造函数。
例如,假设你有一个有三个构造函数的类:
public class MyComponent
{
public MyComponent() { /* ... */ }
public MyComponent(ILogger logger) { /* ... */ }
public MyComponent(ILogger logger, IConfigReader reader) { /* ... */ }
}
现在,如果你像这样在容器中注册组件和服务:
var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>();
builder.RegisterType<ConsoleLogger>().As<ILogger>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope())
{
var component = scope.Resolve<MyComponent>();
}
当你解析组件时,Autofac 会看到你已经注册了 ILogger
,但没有注册 IConfigReader
。在这种情况下,会选择第二个构造函数,因为它具有容器中能找到的最多参数。
关于基于反射的组件的一个重要注意事项: 通过 RegisterType
注册的所有组件类型必须是具体类型。虽然组件可以作为 服务 暴露抽象类或接口,但你不能注册抽象类或接口组件。如果你考虑一下,这是有意义的:在幕后,Autofac 正在创建你注册的东西的实例。你不能 new
一个抽象类或接口。你需要有一个实现,对吧?
指定构造函数
你可以手动选择特定的构造函数并覆盖自动选择,方法是在使用 UsingConstructor
方法和一个表示构造函数参数类型的类型列表时注册你的组件:
builder.RegisterType<MyComponent>()
.UsingConstructor(typeof(ILogger), typeof(IConfigReader));
需要注意的是,你仍然需要在解析时间提供所需的参数,否则尝试解析对象时会出错。你可以 在注册时传递参数 ,也可以 在解析时传递它们 。
注意 有关自定义选择构造函数的高级方法,请参阅 这里 。
必需属性
从 Autofac 7.0 开始,在基于反射的组件中,所有 必需属性 都会自动被解析,就像构造函数参数一样。
组件的所有必需属性都必须是可解析的服务(或作为 参数 提供),否则在尝试解析组件时会抛出异常。
例如,考虑一个具有以下属性的类:
public class MyComponent
{
public required ILogger Logger { protected get; init; }
public required IConfigReader ConfigReader { protected get; init; }
}
你可以像使用具有构造函数的类那样注册和使用这个类:
var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>();
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<ConfigReader>().As<IConfigReader>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope())
{
// Logger 和 ConfigReader将被填充。
var component = scope.Resolve<MyComponent>();
}
必需属性也会自动设置在所有基类上(如果存在),这使得在深层次对象结构中使用必需属性很有用,因为这允许你避免在服务集中调用带有服务的基类构造函数;Autofac 会为你设置基类属性。
注意 有关必需属性注入的更多详细信息,请参阅 属性注入文档 中的专门部分。
实例组件
在某些情况下,你可能希望预先生成对象的实例并将它添加到容器中供注册的组件使用。你可以使用 RegisterInstance
方法来实现这一点:
var output = new StringWriter();
builder.RegisterInstance(output).As<TextWriter>();
当你这样做时,需要考虑的是 Autofac 会 自动处理注册组件的生命周期管理 ,你可能更想控制对象的生命周期,而不是让 Autofac 为你调用 Dispose
。在这种情况下,你需要使用 ExternallyOwned
方法注册实例:
var output = new StringWriter();
builder.RegisterInstance(output)
.As<TextWriter>()
.ExternallyOwned();
将提供的实例注册到容器中对于将 Autofac 集成到现有应用程序并使容器中的组件使用已存在的单例实例也很方便。与其直接将这些组件绑定到单例,不如将其注册为实例:
builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();
这确保最终可以消除静态单例并替换为由容器管理的一个。
实例提供的默认服务是实例的实际类型。请参阅 “服务与组件” 部分。
Lambda 表达式组件
当组件创建逻辑超出简单构造函数调用时,反射是一个很好的默认选择。然而,当事情变得复杂时,就会出现问题。
Autofac 可以接受一个委托或 lambda 表达式作为组件创建者:
builder.Register(c => new A(c.Resolve<B>()));
传递给表达式的参数 c
是组件上下文(一个 IComponentContext
对象),组件在此上下文中创建。你可以使用此上下文来从容器中解析其他值,以协助创建组件。使用这个上下文而不是闭包来访问容器非常重要,以便正确支持 确定性垃圾回收 和嵌套容器。
你还可以使用这个上下文参数来满足额外的依赖关系——在示例中,A
需要一个类型为 B
的构造函数参数,该参数可能有额外的依赖项。
除了使用 IComponentContext
在 lambda 表达式中解决依赖项外,你还可以使用泛型 Register
重载,将依赖项作为 lambda 的可变数量类型参数传递,并让 Autofac 为你解决它们:
builder.Register((IDependency1 dep1, IDependency2 dep2) => new Component(dep1, dep2));
如果你需要做出条件选择,或者使用 ResolveNamed
等方法,只需将 IComponentContext
作为 lambda 的第一个参数:
builder.Register((IComponentContext ctxt, IDependency1 dep1) => new Component(dep1, ctxt.ResolveNamed<IDependency2>("value")));
表达式创建的组件的默认服务是表达式返回类型。
下面是通过反射组件创建效果不佳但通过 lambda 表达式很好地解决的一些需求示例。
复杂参数
构造函数参数不能总是使用简单的常量值声明。与其在 XML 配置语法中费力地构建某种类型值,不如使用代码:
builder.Register(c => new UserSession(DateTime.Now.AddMinutes(25)));
(当然,会话过期可能是一个你想在配置文件中指定的事情——但你明白意思了;)
属性注入
虽然 Autofac 提供了 更高级的属性注入方法 ,但你也可以使用表达式和属性初始化器来填充属性:
builder.Register(c => new A(){ MyB = c.ResolveOptional<B>() });
ResolveOptional
方法会尝试解析值,但如果服务未注册,不会抛出异常。(如果服务已注册但无法正确解析,你仍会收到异常。)这是 解析服务 的选项之一。
在大多数情况下,不推荐使用属性注入。 通过 空对象模式,重载构造函数或构造函数参数的默认值等替代方法,可以使用构造注入创建更干净、"不可变"的组件,同时支持可选依赖项。
根据参数值选择实现
隔离组件创建的一大好处是具体类型可以变化。这通常在运行时而非配置时间完成:
builder.Register<CreditCard>(
(c, p) =>
{
var accountId = p.Named<string>("accountId");
if (accountId.StartsWith("9"))
{
return new GoldCard(accountId);
}
else
{
return new StandardCard(accountId);
}
});
在这个例子中,CreditCard
由 GoldCard
和 StandardCard
两个类实现——具体实例化哪一种取决于运行时提供的账户 ID。
在这个示例中,参数是提供给创建函数的 通过一个名为 p
的可选第二个参数传递参数。
使用这种注册方式如下:
var card = container.Resolve<CreditCard>(new NamedParameter("accountId", "12345"));
你可以使用 Func<X, Y>
关系 ,将参数从命名参数改为类型参数:
builder.Register<CreditCard>(
(c, p) =>
{
// 使用类型参数而不是命名参数以与Func<X, Y>配合
var accountId = p.TypedAs<string>();
if (accountId.StartsWith("9"))
{
return new GoldCard(accountId);
}
else
{
return new StandardCard(accountId);
}
});
这样注册看起来像这样:
var cardFactory = container.Resolve<Func<string, CreditCard>>();
var card = cardFactory("12345");
你也可以使用 lambda 表达式组件 ,使其更简单:
builder.Register<string, CreditCard>(
accountId =>
{
if (accountId.StartsWith("9"))
{
return new GoldCard(accountId);
}
else
{
return new StandardCard(accountId);
}
});
如果你有一个创建 CreditCard
实例的委托,并使用了 delegate 工厂 ,也可以实现另一种干净且类型安全的语法。Delegate 工厂是支持多个相同类型参数的方法。
开放泛型组件
Autofac 支持开放泛型类型。使用 RegisterGeneric()
构建器方法:
builder.RegisterGeneric(typeof(NHibernateRepository<>))
.As(typeof(IRepository<>))
.InstancePerLifetimeScope();
当容器请求匹配的服务类型时,Autofac 会映射为等效的封闭实现类型:
// Autofac 将返回一个 NHibernateRepository <Task>
var tasks = container.Resolve<IRepository<Task>>();
注册特定服务类型(如 IRepository<Person>
)会覆盖开放泛型版本。
如果需要根据自定义行为选择封闭泛型实现,也可以使用委托提供封闭泛型类型:
var builder = new ContainerBuilder();
builder.RegisterGeneric((context, types, parameters) =>
{
// 对选择哪种封闭类型作出决策。
if (types.Contains(typeof(string)))
{
return new StringSpecializedImplementation();
}
return Activator.CreateInstance(typeof(GeneralImplementation<>).MakeGenericType(types));
}).As(typeof(IService<>));
注意 请注意,使用
RegisterGeneric
的委托形式通常比基于反射的形式性能稍低,因为不能以相同方式缓存封闭泛型类型。
服务与组件
当你注册 组件 时,必须告诉 Autofac 该组件暴露哪些 服务 。默认情况下,大多数注册项只自己暴露为注册类型:
// 这个暴露了服务 "CallLogger"
builder.RegisterType<CallLogger>();
组件只能根据它们公开的服务进行 决定 。在简单示例中这意味着:
// 这将工作,因为组件默认暴露其类型:
scope.Resolve<CallLogger>();
// 这将不会工作,因为我们没有告诉注册也把 ILogger 接口暴露在 CallLogger 上:
scope.Resolve<ILogger>();
你可以根据需要为组件暴露任意数量的服务:
builder.RegisterType<CallLogger>()
.As<ILogger>()
.As<ICallInterceptor>();
一旦暴露了服务,就可以根据该服务解析组件。但是,请注意,一旦将组件作为特定服务公开,将默认服务(组件类型)替换掉:
// 这些都将工作,因为我们注册时提供了适当的服务:
scope.Resolve<ILogger>();
scope.Resolve<ICallInterceptor>();
// 这将不再工作,因为我们为组件指定了服务覆盖:
scope.Resolve<CallLogger>();
如果你想根据一组服务以及使用默认服务公开组件,请使用 AsSelf
方法:
builder.RegisterType<CallLogger>()
.AsSelf()
.As<ILogger>()
.As<ICallInterceptor>();
现在所有这些都将工作:
// 这些都将工作,因为我们注册时提供了适当的服务:
scope.Resolve<ILogger>();
scope.Resolve<ICallInterceptor>();
scope.Resolve<CallLogger>();
默认注册
如果有多个组件暴露相同的 服务 ,Autofac 将使用最后一个注册的组件作为该服务的默认提供者:
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<FileLogger>().As<ILogger>();
在这种情况下,FileLogger
将是 ILogger
的默认,因为它是最后注册的。
要更改此行为,请使用 PreserveExistingDefaults()
修饰符:
builder.RegisterType<ConsoleLogger>().As<ILogger>();
builder.RegisterType<FileLogger>().As<ILogger>().PreserveExistingDefaults();
在这种情况下,ConsoleLogger
将是 ILogger
的默认,因为对 FileLogger
的后续注册使用了 PreserveExistingDefaults()
。
条件注册
注意 条件注册是在 Autofac 4.4.0版本中引入的。
大多数情况下,如上所述 "默认注册" 部分所述,重写注册就足以确保在运行时正确解析组件。确保按正确的顺序注册;利用 PreserveExistingDefaults()
;利用 lambda 表达式/委托注册处理更复杂的条件和行为,可以让你走得更远。
然而,在某些场景下,这可能不是你想要的方式:
- 如果有其他功能正在处理,你不想让组件存在于系统中。例如,如果你解析一个
IEnumerable<T>
的服务,所有实现该服务的注册组件都将返回,即使未使用PreserveExistingDefaults()
。通常这没问题,但在某些边缘情况下可能不希望这样。 - 只有在其他组件未注册时,或者只有在其他组件已注册时,你才想注册组件。你不能从正在构建的容器中解析东西,也不应该更新已经构建好的容器。根据其他注册动态注册组件可能会有所帮助。
有两个注册扩展可以帮助解决这些问题:
OnlyIf()
- 提供一个使用IComponentRegistryBuilder
来确定是否应进行注册的 lambda。IfNotRegistered()
- 简化方法,如果其他服务已注册,则阻止注册发生。
这些扩展在 ContainerBuilder.Build()
时运行,并按实际组件注册的顺序执行。以下是一些示例,展示它们如何工作:
var builder = new ContainerBuilder();
// 只有 ServiceA 将被注册。
// 注意 IfNotRegistered 检查的是 SERVICE TYPE(As<T>),而不是 COMPONENT TYPE(RegisterType<T>)。
builder.RegisterType<ServiceA>()
.As<IService>();
builder.RegisterType<ServiceB>()
.As<IService>()
.IfNotRegistered(typeof(IService));
// HandlerA 将被注册 - 它在 HandlerB 有机会注册之前运行,
// 所以 IfNotRegistered 检查将找不到它。
//
// HandlerC 不会被注册,因为它在 HandlerB 之后运行。注意它可以检查 HandlerB 的类型,
// 因为 HandlerB 使用了 AsSelf() 而不是仅 As<IHandler>()。再次强调,
// IfNotRegistered 只能检查 "As" 类型的注册。
builder.RegisterType<HandlerA>()
.AsSelf()
.As<IHandler>()
.IfNotRegistered(typeof(HandlerB));
builder.RegisterType<HandlerB>()
.AsSelf()
.As<IHandler>();
builder.RegisterType<HandlerC>()
.AsSelf()
.As<IHandler>()
.IfNotRegistered(typeof(HandlerB));
// Manager将被注册,因为都注册了IService和HandlerB。OnlyIf谓词允许更大的灵活性。
builder.RegisterType<Manager>()
.As<IManager>()
.OnlyIf(reg =>
reg.IsRegistered(new TypedService(typeof(IService))) &&
reg.IsRegistered(new TypedService(typeof(HandlerB))));
// 对于开放泛型要小心 - IfNotRegistered和IsRegistered仅适用于封闭泛型,因为那是你将要解析的!
// 注意检查的是封闭泛型,它作为开放泛型的一部分将被注册。
// 如果你在IfNotRegistered检查中放入一个开放泛型,它总是会显示未注册
builder.RegisterGeneric(typeof(CommandHandler<>))
.As(typeof(ICommandHandler<>))
.IfNotRegistered(typeof(ICommandHandler<MyCommand>));
// 这是条件检查实际运行的地方。再次强调,它们按照添加到ContainerBuilder的注册顺序运行。
var container = builder.Build();
注册配置
你可以通过 XML 或程序化配置( “模块” )配置同时提供一组注册,或者在运行时更新注册。你还可以 使用 Autofac 模块 来实现动态注册逻辑或条件注册策略。
动态注册
Autofac 模块 是引入动态注册逻辑或简单跨切特性最简便的方式。例如,你可以使用模块来 动态地将 log4net 日志实例附加到正在解决的服务上 。
如果你发现需要更动态的行为,比如添加对新 隐式关系类型的支持 ,你可能需要 查看高级概念部分的注册源部分 。