Pourquoi un type est-il enregistré deux fois lorsque lifetime manager est spécifié?
J'utilise le mécanisme Register by convention D'Unity dans le scénario suivant:
public interface IInterface { }
public class Implementation : IInterface { }
Étant donné la classe Implementation
et son interface, j'exécute RegisterTypes
de la manière suivante:
unityContainer.RegisterTypes(
new[] { typeof(Implementation) },
WithMappings.FromAllInterfaces,
WithName.Default,
WithLifetime.ContainerControlled);
Après cet appel, unitContainer
contient trois enregistrements:
-
IUnityContainer
->IUnityContainer
(ok) -
IInterface
->Implementation
(ok) -
Implementation
->Implementation
(???)
Lorsque je change l'appel comme suit:
unityContainer.RegisterTypes(
new[] { typeof(Implementation) },
WithMappings.FromAllInterfaces,
WithName.Default);
Le conteneur ne contient que deux inscriptions:
-
IUnityContainer
->IUnityContainer
(ok) -
IInterface
->Implementation
(ok)
(c'est le comportement souhaité).
Après avoir jeté un coup d'oeil dans le code source de Unity, j'ai remarqué qu'il y a un malentendu sur la façon dont IUnityContainer.RegisterType
devrait fonctionner.
La méthode RegisterTypes
fonctionne comme suit (les commentaires indiquent quelles sont les valeurs dans les scénarios présentés ci-dessus):
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); // !
}
}
Parce que fromTypes
n'est pas vide, le RegisterTypeMappings
ajoute un mappage de type: IInterface
-> Implementation
(correct).
Ensuite, dans le cas où lifetimeManager
n'est pas null, le code tente de changer le gestionnaire de durée de vie avec l'appel suivant:
container.RegisterType(type, name, lifetimeManager, injectionMembers);
Le nom de cette fonction est complètement trompeur, car la documentation indique clairement que:
RegisterType un LifetimeManager pour le type et le nom donnés avec le conteneur. Aucun mappage de type n'est effectué pour ce type.
, Malheureusement, non seulement le nom est trompeur, mais le la documentation est erronée. Lors du débogage de ce code, j'ai remarqué, que quand il n'y a pas de mappage de type
(Implementation
dans les scénarios présentés ci-dessus), il est ajouté (comme type
-> type
) et c'est pourquoi nous nous retrouvons avec trois inscriptions dans le premier scénario.
J'ai téléchargé les sources D'Unity pour résoudre le problème, mais j'ai trouvé le test unitaire suivant:
[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);
// ...
- ce qui est presque exactement mon cas, et conduit à ma question:
Pourquoi est-ce attendu? C'est conceptuel erreur, un test unitaire créé pour correspondre au comportement existant mais pas nécessairement correct, Ou Est-ce que je manque quelque chose d'important?
J'utilise Unity v4. 0. 30319.
3 réponses
Nous devrions tendre la main aux développeurs originaux pour être sûr, mais c'est ce que je ne peux que supposer quant à la raison pour laquelle il a été codé pour être de cette façon...
Unity a une fonctionnalité qui permet de résoudre des classes concrètes même si le type n'a pas été enregistré. Dans ce scénario, Unity doit faire l'hypothèse que vous voulez le gestionnaire de durée de vie par défaut (transitoire) et aucun membre d'injection pour ce type concret. Si vous n'aimez pas ces stratégies par défaut, vous devez type de béton vous-même et spécifiez votre personnalisation.
Donc, en suivant ce train de pensée, lorsque vous appelez RegisterTypes et que vous spécifiez un gestionnaire de durée de vie et/ou un membre d'injection, alors Unity fait l'hypothèse que vous voulez ce comportement lors de la résolution par l'interface et par le type concret. Mais si vous ne spécifiez pas ces stratégies, Unity n'a pas besoin de l'enregistrement concret car il reviendra au comportement par défaut lors de la résolution du béton type.
La seule autre explication que je peux trouver est pour le bien de plugins. InjectionMember
est extensible, donc Unity pense que vous pourriez transmettre un comportement personnalisé pour un plugin. Et il fait donc l'hypothèse que vous pourriez vouloir que ce comportement personnalisé pour le plugin soit appliqué à la fois à l'interface et au béton lorsque vous vous inscrivez par convention.
Je comprends que vous rencontrez ce problème en essayant de tester l'Unité de votre application bootstrap. Je suppose que vous testez pour vous assurer que les types que vous Enregistrez et que seuls ces types sont enregistrés. Cela brise vos tests parce que certains tests trouvent cet enregistrement concret supplémentaire.
Du point de vue du test unitaire, je dirais que vous allez au-delà de la portée de ce que vous essayez de tester. Si vous commencez à utiliser des plugins, il y aura pas mal d'autres enregistrements qui commencent à apparaître (comme les politiques et les comportements d'interception). Cela va juste ajouter au problème que vous êtes voir maintenant à cause de la façon dont vous testez. Je vous recommande de modifier vos tests pour vous assurer que seule une liste blanche de types sont enregistrés et ignorer tout le reste. Avoir le béton (ou d'autres enregistrements supplémentaires) est bénin à votre demande.
(par exemple, mettez vos interfaces dans la liste blanche et affirmez que chacune d'entre elles est enregistrée et ignorez le fait que le béton est enregistré)
Une raison à laquelle je peux penser pour le comportement actuel est qu'il réutilise la fonctionnalité/implémentation Unity existante et rend la fonctionnalité d'enregistrement par convention assez facile à implémenter.
L'implémentation Unity existante à laquelle je pense est la séparation du mappage de type et du plan de construction en différentes politiques, donc si vous travaillez sur le code source Unity, ce serait une façon courante de penser aux choses. Aussi, d'après ce que j'ai lu dans le passé, la propriété Registrations
on dirait qu'il a été pensé comme un citoyen de deuxième classe et non destiné à être utilisé pour beaucoup plus que le débogage, donc avoir un autre enregistrement n'aurait peut-être pas semblé être une grosse affaire. Peut-être ces deux points réunis faisaient-ils partie de la décision?
Outre l'élément supplémentaire dans les collections D'enregistrements, la fonctionnalité fonctionne dans ce cas.
Ensuite, dans le cas où
lifetimeManager
n'est pas null, le code tente de changer le gestionnaire de durée de vie avec ce qui suit appel
La méthode RegisterTypes
ne définit pas réellement un LifetimeManager
. Si aucun LifetimeManager
n'est spécifié, le type cible concret n'est pas explicitement enregistré et Unity s'appuie sur le comportement par défaut interne lors de la création de l'objet (dans ce cas, un LifetimeManager
par défaut de TransientLifetimeManager
).
Mais si quelque chose est spécifié qui pourrait potentiellement remplacer les valeurs par défaut (c'est-à-dire un LifetimeManager
ou InjectionMembers
) alors le type concret sera explicitement enregistré.
Il devrait être possible d'obtenir L'inscription par Convention de travailler sans l'enregistrement supplémentaire. Cependant, il y a quelques complexités à considérer. Si un type concret implémente de nombreuses interfaces, il peut y avoir plusieurs enregistrements de mappage pour un type concret, mais chaque enregistrement aura besoin de sa propre instance lifetime manager (ils ne peuvent pas être réutilisés). Vous pouvez donc créer de nouvelles instances de gestionnaire de durée de vie pour chaque enregistrement de mappage, mais (je crois) seul le dernier gestionnaire de durée de vie serait utilisé. Donc pour éviter la création d'objets inutiles peut être affecter uniquement le gestionnaire de durée de vie sur le dernier mappage de type? C'est juste un scénario qui m'est venu à l'esprit-il y a une variété de scénarios et de combinaisons à considérer.
Donc, je pense que l'implémentation actuelle est un moyen facile d'implémenter la fonctionnalité sans avoir à gérer spécifiquement les cas de bord avec le seul effet secondaire étant l'enregistrement supplémentaire. Je suppose que cela faisait probablement partie de la réflexion à l'époque (mais je n'étais pas là, donc c'est juste une supposition).
Ce problème a été corrigé dans la version 5.2.1, comme expliqué dans cet article:
Maintenant, toutes les informations transmises à Unity lors de l'enregistrement sont stockées avec
FromType
au lieu deToType
. Donc, enregistrez le type comme ceci:container.RegisterType<ILogger, MockLogger>(new ContainerControlledLifetimeManager(), new InjectionConstructor());
Crée un seul enregistrement
ILogger
et associésLifetimeManager
et tout fourniInjectionMemebers
avec elle. À ce stadeMockLogger
est toujours non enregistré.