2016-01-20 8 views
26

Ich verwende Register Unity vereinbarungs Mechanismus in dem folgende Szenario:Warum wird ein Typ zweimal registriert, wenn der Lifetime Manager angegeben wird?

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

Danach:

public interface IInterface { } 

public class Implementation : IInterface { } 

Bei Implementation Klasse und ihre Schnittstelle I RegisterTypes auf folgende Weise renne Anruf, unitContainer enthält drei Registrierungen:

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

Wenn ich den Anruf wie folgt ändern:

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

Der Container enthält nur zwei Registrierungen:

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

(dies das gewünschte Verhalten).

Nachdem ich in Unity's source code spähen, habe ich bemerkt, dass es einige Missverständnisse darüber gibt, wie IUnityContainer.RegisterType funktionieren sollte.

Die RegisterTypes Methode funktioniert wie folgt (die Kommentare anzuzeigen, was sind die Werte in den Szenarien oben dargestellt):

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

Da fromTypes nicht leer ist, fügt der RegisterTypeMappings eine Typzuordnung: IInterface ->Implementation (richtig).

im Falle Wenn dann lifetimeManager nicht null ist, versucht der Code die Lebensdauer Manager mit dem folgenden Aufruf zu ändern:

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

Der Name dieser Funktion völlig irreführend, weil the documentation eindeutig fest, dass:

RegisterType ein LifetimeManager für den angegebenen Typ und Namen mit dem Container. Für diesen Typ wird keine Typzuordnung durchgeführt.

Leider ist nicht nur der Name irreführend, aber die Dokumentation ist falsch. Wenn ich diesen Code debugge, habe ich festgestellt, dass, wenn es kein Mapping von type (Implementation in den oben dargestellten Szenarien) gibt, es hinzugefügt wird (wie type ->type) und deshalb enden wir mit drei Registrierungen im ersten Szenario .

Ich habe Unity Quellen heruntergeladen, das Problem zu beheben, aber ich habe den folgenden Unit-Test gefunden:

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

    // ... 

- die fast genau mein Fall ist, und führe zu meiner Frage:

Warum wird das erwartet? Ist es ein konzeptioneller Fehler, ein Komponententest, der erstellt wurde, um dem vorhandenen Verhalten zu entsprechen, aber nicht unbedingt korrekt, oder fehlt mir etwas Wichtiges?

Ich verwende Unity v4.0.30319.

Antwort

1

Ein Grund, den ich für das aktuelle Verhalten ist, dass es denken kann, wiederverwendet die vorhandene Unity-Funktionalität/Implementierung und macht die Registrierung per Konvention sehr einfach zu implementieren.

Die vorhandene Unity-Implementierung, an die ich denke, ist die Trennung von Typzuordnung und Erstellungsplan in verschiedene Richtlinien. Wenn Sie also an Unity-Quellcode arbeiten, wäre dies eine übliche Denkweise.Aus dem, was ich in der Vergangenheit gelesen habe, scheint die Registrations Eigenschaft, wie es als Bürger zweiter Klasse gedacht war und nicht für viel mehr als Debugging verwendet werden sollte, so dass eine andere Registrierung nicht eine große Sache zu sein schien . Vielleicht waren diese beiden Punkte Teil der Entscheidung?

Neben dem zusätzlichen Element in den Registrations-Sammlungen funktioniert das Feature in diesem Fall.

im Falle Wenn dann lifetimeManager nicht null ist, versucht der Code die Lebensdauer Manager mit dem folgenden Aufruf wird

Die RegisterTypes Methode eigentlich kein LifetimeManager gesetzt zu ändern. Wenn kein LifetimeManager angegeben ist, wird der konkrete Zieltyp nicht explizit registriert und Unity verlässt sich beim Erstellen des Objekts auf das interne Standardverhalten (in diesem Fall ein Standardwert LifetimeManager von TransientLifetimeManager).

Wenn jedoch etwas angegeben wird, das möglicherweise die Standardeinstellungen überschreiben könnte (d. H. LifetimeManager oder InjectionMembers), wird der konkrete Typ explizit registriert.

Es sollte möglich sein, Registrierung durch Konvention zu erhalten, ohne die zusätzliche Registrierung zu arbeiten. Es gibt jedoch einige Schwierigkeiten zu beachten. Wenn ein konkreter Typ viele Schnittstellen implementiert, kann es mehrere Zuordnungsregistrierungen für einen konkreten Typ geben, aber jede Registrierung benötigt eine eigene Lifetime Manager-Instanz (sie können nicht wiederverwendet werden). Sie können also für jede Zuordnungsregistrierung neue Lifetime Manager-Instanzen erstellen, aber (ich glaube) nur den letzten Lifetime Manager verwenden. Um unnötige Objekterstellung zu vermeiden, wird der Lifetime Manager möglicherweise nur dem letzten Typ zugeordnet. Das ist nur ein Szenario, das mir aufgefallen ist - es gibt eine Vielzahl von Szenarien und Kombinationen zu beachten.

Also ich denke, dass die aktuelle Implementierung eine einfache Möglichkeit ist, das Feature zu implementieren, ohne speziell Edge Fälle behandeln zu müssen, mit dem einzigen Nebeneffekt, der die zusätzliche Registrierung ist. Ich denke, das war wahrscheinlich ein Teil des Denkens zu der Zeit (aber ich war nicht dort, also ist es nur eine Vermutung).

+0

Der Teil über die Notwendigkeit, mehrere Instanzen eines Standard Lifetime Managers zu haben, wenn mehrere Interfaces vorhanden sind, scheint ein starkes Argument zu sein und der einzige konkrete Grund dafür. Vielen Dank! – BartoszKP

7

Wir müssten die ursprünglichen Entwickler erreichen, um sicher zu sein, aber das ist, was ich nur als davon ausgehen kann, warum es codiert wurde auf diese Weise zu sein ...

Unity verfügt über eine Funktion, die konkreten erlaubt Klassen, die aufgelöst werden sollen, obwohl der Typ nicht registriert wurde. In diesem Szenario muss Unity annehmen, dass der Standardlebensdauermanager (transient) und keine Injektionselemente für diesen konkreten Typ verwendet werden sollen. Wenn Ihnen diese Standardrichtlinien nicht gefallen, müssen Sie den konkreten Typ selbst registrieren und Ihre Anpassung angeben.

Nach diesem Gedankengang, wenn Sie RegisterTypes aufrufen und einen Lifetime Manager und/oder ein Injektionselement angeben, nimmt Unity die Annahme vor, dass dieses Verhalten bei der Auflösung durch die Schnittstelle und den konkreten Typ gewünscht wird. Wenn Sie diese Richtlinien jedoch nicht angeben, benötigt Unity keine konkrete Registrierung, da bei der Auflösung des konkreten Typs auf das Standardverhalten zurückgegriffen wird.

Die einzige andere Erklärung, die ich finden kann, ist für plugins. InjectionMember ist erweiterbar, sodass Unity denkt, dass Sie ein benutzerdefiniertes Verhalten für ein Plugin übergeben können. Daher wird davon ausgegangen, dass das benutzerdefinierte Verhalten für das Plug-in sowohl für die Benutzeroberfläche als auch für das Konkrete gelten soll, wenn Sie sich per Konvention registrieren.

Ich verstehe, dass Sie auf dieses Problem stoßen, während Sie versuchen, Ihre Anwendung Bootstrap Einheit zu testen. Ich nehme an, dass Sie testen, um sicherzustellen, dass die Typen, die Sie registrieren, und nur diese Typen registriert sind. Das bricht Ihre Tests, weil einige Tests diese extra konkrete Registrierung finden.

Aus der Sicht eines Komponententests würde ich argumentieren, dass Sie den Rahmen dessen überschreiten, was Sie zu testen versuchen. Wenn du Plugins verwendest, werden einige andere Registrierungen erscheinen (wie Abhörrichtlinien und Verhaltensweisen). Das wird nur zu dem Problem beitragen, das Sie jetzt sehen, aufgrund der Art, wie Sie testen. Ich würde empfehlen, dass Sie Ihre Tests ändern, um sicherzustellen, dass nur eine Whitelist von Typen registriert wird und alles andere ignoriert. Die Betonung (oder andere zusätzliche Registrierungen) ist gut für Ihre Anwendung.

(zB setzen Sie Ihre Schnittstellen in der Whitelist und behaupten, jeder von denen, registriert ist und die Tatsache ignorieren, dass der Beton registriert ist)

+0

Ich schrieb Unit-Tests für einen Bootstrapper meiner Anwendung und das war sehr überraschend. Es macht meine Tests unlesbar - zwei ähnliche Szenarien ergeben unterschiedliche Ergebnisse. TBH, ich verstehe deine Erklärung nicht. Mit nur einer Registrierung können Sie sowohl den Standardlebensdauermanager als auch den angegebenen Standardwartungsmanager verwenden. Sie haben den Kern des Problems korrekt identifiziert: "Unity macht die Annahme, dass Sie dieses Verhalten bei der Auflösung durch die Schnittstelle ** und durch den konkreten Typ ** wollen" - aber das ist die Frage: Warum? Welches Szenario rechtfertigt diese Unterscheidung? Ich verstehe nicht, warum es in irgendeiner Weise hilfreich wäre. – BartoszKP

+0

Aktualisierte Antwort mit einem anderen möglichen Grund (Plugins) und einen Vorschlag zum Ändern Ihrer Komponententests hinzugefügt. – TylerOhlsen

+0

Ich stimme zu, dass Sie testen sollten, was Ihnen wichtig ist. Testen Sie keine mögliche Implementierung. – Batavia