为什么指定生命周期管理器时类型会注册两次?

Why is a type registered twice when lifetime manager is specified?

我在以下场景中使用 Unity 的约定注册机制:

public interface IInterface { }

public class Implementation : IInterface { }

鉴于 Implementation class 及其界面,我按以下方式 运行 RegisterTypes

unityContainer.RegisterTypes(
    new[] { typeof(Implementation) },
    WithMappings.FromAllInterfaces,
    WithName.Default,
    WithLifetime.ContainerControlled);

本次调用后,unitContainer包含三个注册:

当我改变调用如下:

unityContainer.RegisterTypes(
    new[] { typeof(Implementation) },
    WithMappings.FromAllInterfaces,
    WithName.Default);

容器只包含两个注册:

(这是期望的行为)。

在研究 Unity's source code 之后,我注意到对 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 不为空,代码会尝试通过以下调用更改生命周期管理器:

container.RegisterType(type, name, lifetimeManager, injectionMembers);

这个函数的名字完全是误导,因为the documentation明确指出:

RegisterType a LifetimeManager for the given type and name with the container. No type mapping is performed for this type.

不幸的是,不仅名称具有误导性,而且文档也有误。在调试这段代码时,我注意到,当 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 必须假设您需要默认生命周期管理器(瞬态)并且该具体类型没有注入成员。如果您不喜欢这些默认策略,那么您需要自己注册具体类型并指定您的自定义。

所以按照这个思路,当你调用 RegisterTypes 并且你确实指定了一个生命周期管理器 and/or 一个注入成员时,Unity 会假设你在通过接口和具体类型。但是,如果您不指定这些策略,则 Unity 不需要具体注册,因为它会在解析具体类型时回退到默认行为。

我能想到的唯一其他解释是为了插件。 InjectionMember 是可扩展的,因此 Unity 认为您可以为插件传递自定义行为。因此,它假设您可能希望在按照约定注册时将插件的自定义行为应用于界面和具体。


我了解到您在尝试对您的应用程序进行单元测试时 运行 遇到了这个问题 bootstrap。我假设您正在测试以确保您注册的类型并且仅注册这些类型。这会破坏您的测试,因为某些测试会发现这种额外的具体注册。

从单元测试的角度来看,我认为您超出了您尝试测试的范围。如果您开始使用插件,将会有很多其他注册开始出现(例如拦截策略和行为)。由于您的测试方式,这只会增加您现在看到的问题。我建议您更改测试以确保仅注册 white-list 类型并忽略其他所有类型。拥有具体的(或其他额外的注册)对您的应用程序是有益的。

(例如,将您的接口放在 white-list 中并断言每个接口都已注册并忽略具体已注册的事实)

对于当前的行为,我能想到的一个原因是它重用了现有的 Unity functionality/implementation 并使按约定注册功能相当容易实现。

我正在考虑的现有 Unity 实现是将类型映射和构建计划分离到不同的策略中,因此如果您正在处理 Unity 源代码,这将是一种常见的思考方式。另外,从我过去读到的内容来看,Registrations 属性 似乎被认为是第二个 class 公民,除了调试之外,它的用途不多,所以重新注册似乎没什么大不了的。也许这两点加在一起就是决定的一部分?

除了注册集合中的额外项目外,该功能在这种情况下也有效。

Then, in case when lifetimeManager is not null, the code attempts to change the lifetime manager with the following call

RegisterTypes 方法实际上并没有设置 LifetimeManager。如果未指定 LifetimeManager,则具体目标类型未明确注册,Unity 在创建对象时依赖于内部默认行为(在本例中,默认 LifetimeManagerTransientLifetimeManager)。

但是如果指定了任何可能覆盖默认值的内容(即 LifetimeManagerInjectionMembers),那么具体类型将被显式注册。

在没有额外注册的情况下,应该可以让按约定注册工作。但是,需要考虑一些复杂性。如果一个具体类型实现了许多接口,那么一个具体类型可能有多个映射注册,但每个注册都需要它自己的生命周期管理器实例(它们不能被重用)。因此,您可以为每个映射注册创建新的生命周期管理器实例,但(我相信)只会使用最后一个生命周期管理器。因此,为了避免不必要的对象创建,也许只在最后一个类型映射上分配生命周期管理器?这只是我想到的一种情况 -- 有多种情况和组合需要考虑。

所以我认为当前的实现是实现该功能的一种简单方法,无需专门处理任何边缘情况,唯一的副作用是额外注册。我想这可能是当时想法的一部分(但我不在场所以这只是一个猜测)。

此行为已在 5.2.1 版中修复,如 this article 中所述:

Now all information passed to Unity during registration is stored with FromType instead of ToType. So registering type like this:

container.RegisterType<ILogger, MockLogger>(new ContainerControlledLifetimeManager(), new InjectionConstructor());

creates just one registration ILogger and associates LifetimeManager and all provided InjectionMemebers with it. At this point MockLogger is still unregistered.