Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I'm using Unity's Register by convention mechanism in the following scenario:

public interface IInterface { }

public class Implementation : IInterface { }

Given Implementation class and its interface I'm running RegisterTypes in the following way:

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

After this call, unitContainer contains three registrations:

  • IUnityContainer -> IUnityContainer (ok)
  • IInterface -> Implementation (ok)
  • Implementation -> Implementation (???)

When I change the call as follows:

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

The container contains only two registrations:

  • IUnityContainer -> IUnityContainer (ok)
  • IInterface -> Implementation (ok)

(this is the desired behaviour).

After peeking into Unity's source code, I've noticed that there is some misunderstanding about how IUnityContainer.RegisterType should work.

The RegisterTypes method works as follows (the comments indicate what are the values in the scenarios presented above):

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);   // !
    }
}

Because fromTypes is not empty, the RegisterTypeMappings adds one type mapping: IInterface -> Implementation (correct).

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

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

This function's name is completely misleading, because the documentation clearly states that:

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

Unfortunately, not only the name is misleading but the documentation is wrong. When debugging this code, I've noticed, that when there is no mapping from type (Implementation in the scenarios presented above), it is added (as type -> type) and that's why we end up with three registrations in the first scenario.

I've downloaded Unity's sources to fix the problem, but I've found the following unit test:

[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);

    // ...

- which is almost exactly my case, and leads to my question:

Why is this expected? Is it a conceptual mistake, a unit test created to match existing behaviour but not necessarily correct, or am I missing something important?

I'm using Unity v4.0.30319.

like image 446
BartoszKP Avatar asked Jan 20 '16 17:01

BartoszKP


People also ask

What is ContainerControlledLifetimeManager?

ContainerControlledLifetimeManager. Creates a singleton object first time you call the Resolve or ResolveAll method and then returns the same object on subsequent Resolve or ResolveAll calls.

How do I register a type with Unity container?

Before Unity resolves the dependencies, we need to register the type-mapping with the container, so that it can create the correct object for the given type. Use the RegisterType() method to register a type mapping. Basically, it configures which class to instantiate for which interface or base class.

What is Hierarchicallifetimemanager?

A special lifetime manager which works like ContainerControlledLifetimeManager, except that in the presence of child containers, each child gets it's own instance of the object, instead of sharing one in the common parent.

What is RegisterType?

RegisterType a type mapping with the container, where the created instances will use the given LifetimeManager. Namespace: Microsoft.Practices.Unity. Assembly: Microsoft.Practices.Unity (in Microsoft.Practices.Unity.dll)


3 Answers

We would need to reach out to the original developers to be sure, but this is what I can only assume as to why it was coded to be this way...

Unity has a feature that allows concrete classes to be resolved even though the type has not been registered. In this scenario, Unity must make the assumption that you want the default lifetime manager (transient) and no injection members for that concrete type. If you don't like those default policies, then you need to register the concrete type yourself and specify your customization.

So following that train of thought, when you call RegisterTypes and you do specify a lifetime manager and/or an injection member, then Unity makes the assumption that you want that behavior when resolving by the interface and by the concrete type. But if you do not specify those policies, then Unity doesn't need the concrete registration because it will fall back to the default behavior when resolving the concrete type.

The only other explanation that I can come up with is for the sake of plugins. InjectionMember is extensible, so Unity thinks you could be passing in a custom behavior for a plugin. And it therefore makes the assumption that you might want that custom behavior for the plugin to be applied to both the interface and the concrete when you are registering by convention.


I understand you are running into this issue while attempting to unit test your application bootstrap. I'm assuming you are testing to ensure the types you register and only those types are registered. This is breaking your tests because some tests find this extra concrete registration.

From a unit test perspective, I would argue that you are going beyond the scope of what you are attempting to test. If you start using plugins, there will be quite a few other registrations that start appearing (like interception policies and behaviors). That will just add to the problem you are seeing now because of the way you are testing. I would recommend you change your tests to ensure only a white-list of types are registered and ignore everything else. Having the concrete (or other additional registrations) is benign to your application.

(e.g. put your interfaces in the white-list and assert each one of those is registered and ignore the fact that the concrete is registered)

like image 192
TylerOhlsen Avatar answered Oct 17 '22 15:10

TylerOhlsen


This behavior was fixed in version 5.2.1, as explained in 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.

like image 44
BartoszKP Avatar answered Oct 17 '22 14:10

BartoszKP


One reason that I can think of for the current behavior is that it reuses the existing Unity functionality/implementation and makes the registration by convention feature fairly easy to implement.

The existing Unity implementation that I'm thinking of is the separation of type mapping and build plan into different policies so if you are working on Unity source code that would be a common way to think about things. Also, from what I've read in the past, the Registrations property seems like it was thought of as a second class citizen and not intended to be used for much more than debugging so having another registration might not have seemed to be a big deal. Perhaps those two points put together were part of the decision?

Besides the extra item in the Registrations collections the feature is working in this case.

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

The RegisterTypes method doesn't actually set a LifetimeManager. If no LifetimeManager is specified then the concrete target type is not explicitly registered and Unity relies on the internal default behavior when creating the object (in this case a default LifetimeManager of TransientLifetimeManager).

But if anything is specified that could potentially override the defaults (i.e. a LifetimeManager or InjectionMembers) then the concrete type will be explicitly registered.

It should be possible to get Registration by Convention to work without the extra registration. However, there are some complexities to consider. If a concrete type implements many interfaces there could be multiple mapping registrations for a concrete type but each registration will need it's own lifetime manager instance (they can't be reused). So you could create new lifetime manager instances for each mapping registration but (I believe) only the last lifetime manager would be used. So to avoid unnecessary object creation perhaps only assign the lifetime manager on the last type mapping? That's just one scenario that occurred to me -- there are a variety of scenarios and combinations to consider.

So I think the current implementation is an easy way to implement the feature without having to specifically handle any edge cases with the only side effect being the extra registration. I would guess that was probably part of the thinking at the time (but I wasn't there so it's just a guess).

like image 1
Randy supports Monica Avatar answered Oct 17 '22 14:10

Randy supports Monica