c# - type - unity containercontrolledlifetimemanager
¿Por qué un tipo se registra dos veces cuando se especifica el administrador de por vida? (3)
Deberíamos comunicarnos con los desarrolladores originales para estar seguros, pero esto es lo que solo puedo asumir por qué fue codificado para ser así ...
Unity tiene una característica que permite resolver clases concretas aunque el tipo no se haya registrado. En este escenario, Unity debe suponer que desea el administrador de duración predeterminado (transitorio) y no miembros de inyección para ese tipo concreto. Si no le gustan esas políticas predeterminadas, entonces necesita registrar el tipo concreto y especificar su personalización.
Entonces, siguiendo esa línea de pensamiento, cuando llama a RegisterTypes y especifica un administrador de por vida y / o un miembro de inyección, entonces Unity asume que desea ese comportamiento cuando se resuelve por la interfaz y por el tipo concreto. Pero si no especifica esas políticas, entonces Unity no necesita el registro concreto porque volverá al comportamiento predeterminado al resolver el tipo concreto.
La única otra explicación que se me ocurre es por los complementos. InjectionMember
es extensible, por lo que Unity cree que podrías estar pasando un comportamiento personalizado para un complemento. Y, por lo tanto, supone que es posible que desee que ese comportamiento personalizado para el complemento se aplique tanto a la interfaz como al concreto cuando se registre por convención.
Entiendo que se está encontrando con este problema al intentar realizar una prueba unitaria de su programa de arranque. Supongo que está realizando pruebas para garantizar los tipos que registra y solo esos tipos están registrados. Esto está rompiendo sus pruebas porque algunas pruebas encuentran este registro concreto adicional.
Desde una perspectiva de prueba unitaria, diría que va más allá del alcance de lo que está intentando probar. Si comienza a usar complementos, habrá algunos otros registros que comenzarán a aparecer (como políticas y comportamientos de intercepción). Eso solo aumentará el problema que está viendo ahora debido a la forma en que está realizando las pruebas. Le recomendaría que cambie sus pruebas para asegurarse de que solo se registre una lista blanca de tipos e ignore todo lo demás. Tener el concreto (u otros registros adicionales) es benigno para su aplicación.
(Por ejemplo, ponga sus interfaces en la lista blanca y afirme que cada una de ellas está registrada e ignore el hecho de que el concreto está registrado)
Estoy usando el mecanismo de registro por convención de Unity en el siguiente escenario:
public interface IInterface { }
public class Implementation : IInterface { }
Dada la clase de Implementation
y su interfaz, estoy ejecutando RegisterTypes
de la siguiente manera:
unityContainer.RegisterTypes(
new[] { typeof(Implementation) },
WithMappings.FromAllInterfaces,
WithName.Default,
WithLifetime.ContainerControlled);
Después de esta llamada, unitContainer
contiene tres registros:
-
IUnityContainer
->IUnityContainer
(ok) -
IInterface
->Implementation
(ok) -
Implementation
->Implementation
(???)
Cuando cambio la llamada de la siguiente manera:
unityContainer.RegisterTypes(
new[] { typeof(Implementation) },
WithMappings.FromAllInterfaces,
WithName.Default);
El contenedor contiene solo dos registros:
-
IUnityContainer
->IUnityContainer
(ok) -
IInterface
->Implementation
(ok)
(Este es el comportamiento deseado).
Después de echar un vistazo al código fuente de Unity , he notado que hay algunos malentendidos sobre cómo debería funcionar IUnityContainer.RegisterType
.
El método RegisterTypes
funciona de la siguiente manera (los comentarios indican cuáles son los valores en los escenarios presentados anteriormente):
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); // !
}
}
Debido a que fromTypes
no está vacío, el RegisterTypeMappings
agrega una asignación de tipo: IInterface
-> Implementation
(correcta).
Luego, en caso de que lifetimeManager
no sea nulo, el código intenta cambiar el administrador de vida con la siguiente llamada:
container.RegisterType(type, name, lifetimeManager, injectionMembers);
El nombre de esta función es completamente engañoso, porque la documentación indica claramente que:
RegisterType un LifetimeManager para el tipo y nombre dado con el contenedor. No se realiza ningún tipo de mapeo para este tipo.
Desafortunadamente, no solo el nombre es engañoso, sino que la documentación es incorrecta. Al depurar este código, he notado que cuando no hay mapeo del type
( Implementation
en los escenarios presentados anteriormente), se agrega (como type
-> type
) y es por eso que terminamos con tres registros en el primer escenario .
He descargado las fuentes de Unity para solucionar el problema, pero he encontrado la siguiente prueba de unidad:
[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);
// ...
- que es casi exactamente mi caso, y lleva a mi pregunta:
¿Por qué se espera esto? ¿Es un error conceptual, una prueba de unidad creada para coincidir con el comportamiento existente pero no necesariamente correcta, o me falta algo importante?
Estoy usando Unity v4.0.30319.
Este comportamiento se corrigió en la versión 5.2.1, como se explica en este artículo :
Ahora toda la información que se pasa a Unity durante el registro se almacena con
FromType
lugar deToType
. Así que registrando el tipo como este:
container.RegisterType<ILogger, MockLogger>(new ContainerControlledLifetimeManager(), new InjectionConstructor());
crea solo un registro
ILogger
y asociaLifetimeManager
y todos losInjectionMemebers
proporcionados con él. En este punto,MockLogger
aún no está registrado.
Una de las razones por las que puedo pensar en el comportamiento actual es que reutiliza la funcionalidad / implementación existente de Unity y hace que la función de registro por convención sea bastante fácil de implementar.
La implementación de Unity existente en la que estoy pensando es la separación del mapeo de tipos y el plan de compilación en diferentes políticas, por lo que si está trabajando en el código fuente de Unity, esa sería una forma común de pensar en las cosas. Además, por lo que he leído en el pasado, la propiedad de los Registrations
parece que se pensó como un ciudadano de segunda clase y no estaba destinada a ser utilizada para mucho más que la depuración, por lo que tener otro registro podría no haber sido un gran problema. . Tal vez esos dos puntos juntos fueron parte de la decisión?
Además del elemento adicional en las colecciones de registros, la característica está funcionando en este caso.
Luego, en caso de que el
lifetimeManager
no sea nulo, el código intenta cambiar el administrador de tiempo de vida con la siguiente llamada
El método RegisterTypes
no establece realmente un LifetimeManager
. Si no se especifica LifetimeManager
, el tipo de destino concreto no se registra explícitamente y Unity se basa en el comportamiento interno predeterminado al crear el objeto (en este caso, un LifetimeManager
predeterminado de TransientLifetimeManager
).
Pero si se especifica algo que podría anular los valores predeterminados (es decir, un LifetimeManager
o InjectionMembers
), el tipo concreto se registrará explícitamente.
Debería ser posible obtener Registro por convención para trabajar sin el registro adicional. Sin embargo, hay algunas complejidades a considerar. Si un tipo concreto implementa muchas interfaces, puede haber múltiples registros de mapeo para un tipo concreto, pero cada registro necesitará su propia instancia de administrador de por vida (no se pueden reutilizar). Así que podría crear nuevas instancias de administrador de por vida para cada registro de mapeo pero (creo) solo se usaría el último administrador de por vida. Entonces, para evitar la creación innecesaria de objetos, ¿quizás solo asigne el administrador de vida útil en el último tipo de mapeo? Ese es solo un escenario que se me ocurrió: hay una variedad de escenarios y combinaciones a considerar.
Así que creo que la implementación actual es una forma fácil de implementar la función sin tener que manejar específicamente ningún caso de borde con el único efecto secundario que es el registro adicional. Supongo que eso era probablemente parte del pensamiento en ese momento (pero no estaba allí, así que es solo una suposición).