为什么在指定生存期管理器时,一个类型注册两次
本文关键字:类型 一个 注册 两次 生存期 管理器 为什么 | 更新日期: 2023-09-27 18:31:07
我在以下情况下使用 Unity 的按约定寄存器机制:
public interface IInterface { }
public class Implementation : IInterface { }
给定Implementation
类及其接口,我通过以下方式运行RegisterTypes
:
unityContainer.RegisterTypes(
new[] { typeof(Implementation) },
WithMappings.FromAllInterfaces,
WithName.Default,
WithLifetime.ContainerControlled);
在此调用之后,unitContainer
包含三个注册:
-
IUnityContainer
->IUnityContainer
(正常) -
IInterface
->Implementation
(正常) -
Implementation
->Implementation
(???)
当我按如下方式更改调用时:
unityContainer.RegisterTypes(
new[] { typeof(Implementation) },
WithMappings.FromAllInterfaces,
WithName.Default);
容器仅包含两个注册:
-
IUnityContainer
->IUnityContainer
(正常) -
IInterface
->Implementation
(正常)
(这是所需的行为)。
在浏览了Unity的源代码之后,我注意到IUnityContainer.RegisterType
应该如何工作存在一些误解。
RegisterTypes
方法的工作原理如下(注释指示上面介绍的方案中的值是什么):
foreach (var type in types)
{
var fromTypes = getFromTypes(type); // { IInterface }
var name = getName(type); // null
var lifetimeManager = getLifetimeManager(type); // null or ContainerControlled
var injectionMembers = getInjectionMembers(type).ToArray(); // null
RegisterTypeMappings(container, overwriteExistingMappings, type, name, fromTypes, mappings);
if (lifetimeManager != null || injectionMembers.Length > 0)
{
container.RegisterType(type, name, lifetimeManager, injectionMembers); // !
}
}
由于fromTypes
不为空,因此RegisterTypeMappings
添加一个类型映射:IInterface
-> Implementation
(正确)。
然后,如果lifetimeManager
不为 null,代码将尝试使用以下调用更改生存期管理器:
container.RegisterType(type, name, lifetimeManager, injectionMembers);
此函数的名称完全具有误导性,因为文档明确指出:
注册在容器中为给定类型和名称键入生命周期管理器。不会为此类型执行类型映射。
不幸的是,不仅名称具有误导性,而且文档也是错误的。在调试此代码时,我注意到,当没有来自type
的映射时(在上面介绍的场景中Implementation
),它被添加(如 type
-> type
),这就是为什么我们最终在第一个场景中有三个注册。
我已经下载了Unity的源代码来解决这个问题,但我发现了以下单元测试:
[TestMethod]
public void RegistersMappingAndImplementationTypeWithLifetimeAndMixedInjectionMembers()
{
var container = new UnityContainer();
container.RegisterTypes(new[] { typeof(MockLogger) }, getName: t => "name", getFromTypes: t => t.GetTypeInfo().ImplementedInterfaces, getLifetimeManager: t => new ContainerControlledLifetimeManager());
var registrations = container.Registrations.Where(r => r.MappedToType == typeof(MockLogger)).ToArray();
Assert.AreEqual(2, registrations.Length);
// ...
- 这几乎完全是我的情况,并导致了我的问题:
为什么会这样?这是一个概念错误,一个为匹配现有行为而创建的单元测试,但不一定正确,还是我错过了一些重要的东西?
我正在使用 Unity v4.0.30319。
我们需要联系原始开发人员才能确定,但这是我只能假设为什么它被编码成这样......
Unity 具有一项功能,即使类型尚未注册,也允许解析具体类。 在此方案中,Unity 必须假设您需要默认生存期管理器(瞬态),并且不需要该具体类型的注入成员。 如果不喜欢这些默认策略,则需要自己注册具体类型并指定自定义项。
因此,按照这个思路,当您调用 RegisterType 并指定生存期管理器和/或注入成员时,Unity 会假设您希望在通过接口和具体类型进行解析时具有该行为。 但是,如果未指定这些策略,则 Unity 不需要具体注册,因为它将在解析具体类型时回退到默认行为。
我能想出的唯一其他解释是为了插件。 InjectionMember
是可扩展的,因此 Unity 认为您可以为插件传递自定义行为。 因此,它假设您可能希望在按照惯例注册时将插件的自定义行为应用于接口和具体内容。
我知道您在尝试对应用程序引导程序进行单元测试时遇到了此问题。 我假设您正在测试以确保您注册的类型,并且仅注册这些类型。 这打破了您的测试,因为某些测试会发现这种额外的具体注册。
从单元测试的角度来看,我认为你超出了你试图测试的范围。 如果您开始使用插件,将会有相当多的其他注册开始出现(例如拦截策略和行为)。 由于您的测试方式,这只会增加您现在看到的问题。 我建议您更改测试以确保仅注册类型的白名单并忽略其他所有内容。 拥有具体(或其他额外注册)对您的申请是良性的。
(例如,将您的接口放入白名单并断言每个接口都已注册,而忽略混凝土已注册的事实)
此行为已在版本 5.2.1 中修复,如本文中所述:
现在,注册期间传递给Unity的所有信息都存储在
FromType
而不是ToType
.所以注册类型是这样的:container.RegisterType<ILogger, MockLogger>(new ContainerControlledLifetimeManager(), new InjectionConstructor());
仅创建一个注册
ILogger
并关联LifetimeManager
并且都为InjectionMemebers
提供了它。此时MockLogger
仍未注册。
对于当前行为,我能想到的一个原因是它重用了现有的 Unity 功能/实现,并使按约定注册功能相当容易实现。
我正在考虑的现有 Unity 实现是将类型映射和构建计划分离到不同的策略中,因此如果您正在处理 Unity 源代码,这将是思考问题的常见方式。此外,从我过去读到的内容来看,Registrations
属性似乎被认为是二等公民,并且不打算用于调试以外的其他用途,因此进行另一个注册似乎没什么大不了的。也许这两点放在一起是决定的一部分?
除了注册集合中的额外项目外,该功能在这种情况下还可以使用。
然后,如果
lifetimeManager
不为 null,代码将尝试使用以下调用更改生存期管理器
RegisterTypes
方法实际上并没有设置LifetimeManager
。如果未指定LifetimeManager
则不会显式注册具体目标类型,Unity 在创建对象时依赖于内部默认行为(在本例中为 TransientLifetimeManager
的默认LifetimeManager
)。
但是,如果指定了任何可能覆盖默认值的内容(即LifetimeManager
或InjectionMembers
),则将显式注册具体类型。
应该可以在没有额外注册的情况下按公约进行注册。但是,需要考虑一些复杂性。如果具体类型实现了许多接口,则具体类型可能有多个映射注册,但每个注册都需要它自己的生存期管理器实例(它们不能重用)。因此,您可以为每个映射注册创建新的生命周期管理器实例,但(我相信)只会使用最后一个生命周期管理器。因此,为了避免不必要的对象创建,也许只在最后一个类型映射上分配生命周期管理器?这只是我想到的一个场景——有各种各样的场景和组合需要考虑。
因此,我认为当前的实现是实现该功能的简单方法,而无需专门处理任何边缘情况,唯一的副作用是额外的注册。我猜这可能是当时想法的一部分(但我不在那里,所以这只是一个猜测)。