Dependency Injection in Schleupen.CS
In Schleupen.CS kommt an sehr vielen Stellen das Strategiemuster zum Einsatz, wobei die Strategie per Exkurs: Dependency Injection (Konzept) im Standard per Konstruktur injiziert wird. Damit können dann die Klassen unabhängig voneinander implementiert und ein Mock zum Testen einfach eingebunden werden. Zur Laufzeit findet die Bindung dann per DI-Container statt.
In Schleupen.CS wird der DI-Container Windsor | Castle Project verwendet.
Das Strategiemuster hilft, die Richtungsabhängigkeit, insbesondere beim Übergang vom Application Core zur Infrastructure-Schale, im Sinne des Onion-Modells zu lösen. Hier muss zur Laufzeit der Usecase Controller ein Repository verwenden, das er zur Compile-Zeit nicht kennen darf.
Durch Einziehen einer Schnittstelle kann - wie in Onion-Modell beschrieben - die Abhängigkeitsbeziehung gedreht werden (siehe Exkurs: Dependency-Injection). Der DI-Container setzt die Objekthierarchien dann zur Laufzeit zusammen.
Da DI nicht nur hilft die Abhängigkeitsbeziehung korrekt zu implementieren, sondern auch einfach andere Strategien insbesondere für Tests nutzen zu können, findet DI gepaart mit dem Strategiemuster an zahllosen Stellen in der Mikroarchitektur Anwendung.
In Schleupen.CS werden keine Domänenobjekte / Aggregate per DI-Container erzeugt, da diese per Definition keine technologischen Abhängigkeiten zur einen Seite haben und per EntityId auf der anderen Seite entkoppelt sind. Zudem kann das Aufdrahten in performance-intensiven Bereichen zu langsam sein.
Im Architektur-Template von Schleupen.CS für Länder werden Schnittstelle und Implementierung per Konvention verknüpft (Convention over Configuration). Hierbei greift folgende Namenskonvention: für die zu registrierende Schnittstelle namens IBuecherAusleihenService
wird die Implementierung ohne 'I' , also BuecherAusleihenService
, gesucht und im DI-Container registriert. Die Registrierung erfolgt per Default mit dem Lifestyle per WCF-Operation, was bedeutet, dass an allen Stellen während eines Servicesaufrufs dieselbe Instanz verwendet wird.
Implementiert wird diese Konvention im ServiceHostFactoryConfigurator
, die den Anknüpfungspunkt für die Dependency Injection darstellt. Über die Property Assemblies
wird in den spezifizierten Assemblys nach passenden Schnittstellen- und Implementierungstypen gesucht. Den Anschluss zeigt der folgende Code:
public class ServiceHostFactoryConfigurator : WcfContainerConfigurator { protected override IEnumerable<Assembly> Assemblies { get { yield return Assembly.Load(AssemblyNames.Services); yield return Assembly.Load(AssemblyNames.Contracts); yield return Assembly.Load(AssemblyNames.BusinessCases); yield return Assembly.Load(AssemblyNames.Repositories); yield return Assembly.Load(AssemblyNames.Gateways); yield return Assembly.Load(AssemblyNames.GatewaysContracts); } } protected override void OnContainerConfigured(IWindsorContainer windsorContainer) { ... } }
Das Aufdrahten der Objekte eines Service wie in nachfolgender Abbildung dargestellt erfolgt dann in folgenden Schritten:
- Die Verarbeitung des
Request
wird "unterbrochen". - Für die Verarbeitung des
Request
wird eine Instanz vonIService
benötigt.IService
wird durch die KlasseService
implementiert und diese durch die implementierte Konvention aufgelöst. Service
hat folgende Abhängigkeiten:IAssembler
,IUseCaseController
. Analog werden diese Abhängigkeiten rekursiv aufgelöst:- für
IAssembler
wirdAssembler
instanziiert - für
IUseCaseController
wirdUseCaseController
instanziiert
- für
- Die Klasse
Service
wird erzeugt, dabei werden die zuvor erstellten Instanzen vonAssembler
undUseCaseController
im Konstrukutor injiziert.
Alle Bausteine werden auf diese Weise zusammengestellt. Hinzu kommen APIs, beispielsweise des Schleupen.CS-Frameworks oder das API der Transcational Outbox, die per Castle-Installer angeschlossen werden. Dies erfolgt in der Methode OnContainerConfigured
des ServiceHostFactoryConfigurator
s, die nach dem Default-Registrierung ausgeführt wird.
public class ServiceHostFactoryConfigurator : WcfContainerConfigurator { protected override IEnumerable<Assembly> Assemblies { get { ... } } protected override void OnContainerConfigured(IWindsorContainer windsorContainer) { if (windsorContainer == null) { throw new ArgumentNullException(nameof(windsorContainer)); } windsorContainer.Install(new IdempotencyInstaller(IsInsideWcfOperation)); windsorContainer.Install(new PersistenceInstaller(IsInsideWcfOperation) { EnablePersistenceContext = true }); windsorContainer.Install(new CS.PI.Framework.HideableEntities.Persistence.NHibernate.PersistenceInstaller(IsInsideWcfOperation) { EnableHideableEntities = true }); windsorContainer.Install(new DomainEventsInstaller(IsInsideWcfOperation, typeof(RaiseBusinessEventWhenBuecherAusgeliehenDomainEventHandler))); windsorContainer.Install(new ServiceBusClientInstaller(IsInsideWcfOperation) { DatabaseSchema = DatabaseInfo.DatabaseSchema }); windsorContainer.Install(new EnhancedWcfServiceBehaviorInstaller(IsInsideWcfOperation)); windsorContainer.Install(new TransactionalOutboxInstaller(IsInsideWcfOperation) { EnableBusinessEvents = true, // Aktiviert die Unterstützung für Business Events EnableCommandMessages = true, // Aktiviert die Unterstützung für Command Messages }); windsorContainer.Install(new ConfigurationClientInstaller(IsInsideWcfOperation)); windsorContainer.Install(new ChangeLogClientInstaller(IsInsideWcfOperation)); windsorContainer.Install(new ChangeLogServiceInstaller(IsInsideWcfOperation)); windsorContainer.Install(new ApplicationInfrastructureUserClientInstaller(IsInsideWcfOperation)); windsorContainer.RegisterFactory<IAusleiheRepositoryFactory>() .For<IAusleiheRepository>() .ImplementedBy(Assembly.Load(AssemblyNames.Repositories).GetType("Schleupen.AS.MT.BIB.Ausleihen.AusleiheRepository")); base.OnContainerConfigured(windsorContainer); } }
Der Anschluss des DI-Containers im IIS zur Erzeugung einer Service-Instanz erfolgt über die Klasse ServiceHostFactory<TConfigurator>
, die den ServiceHostFactoryConfigurator
als generischen Typparameter entgegen nimmt. Diese wird in der web.config zur Service-Aktivierung angeschlossen. Sie ist der Einstiegspunkt der Konfiguration des DI-Containers:
<?xml version="1.0" encoding="utf-8"?> <configuration> ... <serviceHostingEnvironment> <serviceActivations> ... <add service="Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihen.ActivityService.V3_3.BuecherAusleihenActivityService" relativeAddress="Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusleihenActivityService_3.3.BuecherAusleihenActivityService.svc" factory="Schleupen.AS.MT.BIB.ServiceHostFactory" /> ... </serviceActivations> </serviceHostingEnvironment> </system.serviceModel> </configuration>
Zu guter Letzt muss man einen Plugin-Mechanismus zur Einbindung externer Bibliotheken von dem hier beschriebenen Verfahren abgrenzen. Bei dem hier beschriebenen Verfahren werden die Assemblys in denselben Prozess eingeladen. Die Isolation ist aber i.A. für Plugins zu gering, da diese dieselben Bibliotheken in einer anderen Version mitbringen können.
Als Empfehlung sollte Constructor Injection für Pflichtabhängigkeiten und Property Injection für optionale Abhängigkeiten verwendet werden. Dieses Konzept lässt sich für Konstruktoren im Allgemeinen erweitern - die Nutzung vereinfacht sich deutlich, wenn die Konvention bekannt ist.