Dependency Injection in Umbraco

Samstag, 15.12.2018

Mirko Matytschak

In der Software-Entwicklung gibt es eine Handvoll Konzepte, die unbedingt befolgt werden müssen, um eine Qualitätssicherung sicherzustellen. Eines dieser Konzepte ist die Inversion of Control – auch Dependency Injection genannt. In diesem Artikel zeigen wir, wie Dependency Injection in Umbraco realisiert werden kann.

Das Konzept der Dependency Injection ist schnell erklärt. Das Projekt wird in kleine, handsame Teilaufgaben zerlegt, die man einzeln testen kann. Für jede dieser Teilaufgaben gibt es ein Interface, das dann von anderen Teilaufgaben genutzt wird, um die Komponente anzusprechen, in der die Teilaufgabe implementiert ist. Wegen der durchgängigen Nutzung von Interfaces kann jede Komponente gegen eine andere Komponente mit dem gleichen Interface ausgetauscht werden.

Damit lassen sich für Testszenarios eigene Testkomponenten entwickeln, sogenannte Mocks, die für den Testzweck die ursprünglichen Komponenten ersetzen. Der Nutzen liegt auf der Hand: Der Test muss sich nicht um die Funktionstüchtigkeit der anderen Komponenten scheren, mit denen Online-Anbindungen, Datenbankzugriffe oder sonstige komplexe Aufgaben realisiert werden sollen.

Auftritt: Container

Doch irgendwann stellt sich die Aufgabe, die konkreten Komponenten zusammenzustellen, die man für einen bestimmten Test oder auch für die Produktivumgebung braucht. Damit die Komponenten dies nicht selbst tun müssen (und damit die Abhängigkeiten festlegen), geschieht das durch eine Container-Implementierung. Diesem Container kann gesagt werden, welche Interfaces durch welche Komponenten realisiert werden. Wenn dann die Anwendung läuft, holen sich die Komponenten ihre Abhängigkeiten aus dem Container.

Eine Abhängigkeit aufzulösen, kostet jedoch Zeit. Und so ist man schon früh auf die Lösung gekommen, dass die Abhängigkeiten beim Anlegen einer Komponente dieser gleich mitgegeben werden. Auf eine magische Art und Weise mischt sich der Container nun in den Prozess des Anlegens der Komponenten ein und schiebt ihnen die Instanzen der Komponenten unter, von denen die jeweilige Komponente abhängig ist.

Es gibt mittlerweile für .NET jede Menge Packages, die Container implementieren, wie Autofac, Unity, Ninject etc. Sehr leistungsfähig und sehr groß ist Autofac. Die Lernkurve ist allerdings etwas höher, als bei anderen Implementierungen. Sehr schlank und auch schnell ist Unity, das vor einigen Jahren von einer Gruppe bei Microsoft entwickelt wurde, die sich „Patterns and Practices“ nannte. Mittlerweile wird das Projekt von herstellerunabhängigen Personen geführt.

Wir haben uns für Unity entschieden, weil es schnell und schlank ist und die vorhandenen Strukturen unserer Projekte weitgehend unangetastet lässt.

Installation

Bei der Installation folgten wir einem Grundmuster, das hier beschrieben ist:

https://our.umbraco.com/documentation/reference/using-ioc

Dort gibt es ein Kapitel über Unity. Allerdings ist der Text etwas veraltet, der Code funktioniert nicht, wie angegeben. Deshalb wollen wir hier eine funktionsfähige Lösung beschreiben und ein paar Erklärungen nachreichen, sodass Sie im Fall des Falles diese Lösung an Ihre Gegebenheiten anpassen können.

Um nachvollziehen zu können, was ich im Folgenden beschreibe, erstellen Sie ein leeres ASP.NET Web-Projekt. Laden Sie dann zuerst Umbraco mit

install-package UmbracoCms

Kompilieren Sie und starten Sie das Projekt ohne Debugger. Sie landen im Installations-Prozess und erstellen dort eine Umbraco-Instanz ohne Starterkit (also kein Initial Content).

Testen Sie kurz aus, ob das Backoffice erreichbar ist.

Dann installieren Sie folgende Packages mit install-package:

  • Unity
  • Unity.Mvc
  • Unity.AspNet.WebApi.

Dabei entstehen unter anderem drei Dateien im App_Start-Verzeichnis. Die UnityConfig.cs beschreiben wir später. Die beiden anderen Dateien (UnityMvcActivator.cs, UnityWebApiActivator.cs) sind zwei Versionen der gleichen Funktionalität. Sie können eine davon löschen und belassen die andere Datei mit folgendem Code:

using UmbracoUnity;
 
[assembly: WebActivatorEx.PreApplicationStartMethod( typeof( UnityActivator ), "Start" )]
[assembly: WebActivatorEx.ApplicationShutdownMethod( typeof( UnityActivator ), "Shutdown" )]
 
/// <summary>Provides the bootstrapping for integrating Unity when it is hosted in ASP.NET.</summary>
public static class UnityActivator
{
    /// <summary>Integrates Unity when the application starts.</summary>
    public static void Start()
    {
    }
 
    /// <summary>Disposes the Unity container when the application is shut down.</summary>
    public static void Shutdown()
    {
        var container = UnityConfig.Container;
        container.Dispose();
    }
}

Der Namespace UmbracoUnity ist der von uns für das Projekt gewählte Name. Sie geben stattdessen den Default Namespace Ihres Projekts an.

Nun muss Unity irgendwie an die Applikation angebunden werden. In Umbraco gibt es zwei Möglichkeiten, wie man sich in den Applikations-Start einmischen kann. Wir haben die Methode gewählt, bei der das Interface IApplicationEventHandler implementiert wird. Umbraco sucht beim Systemstart alle Implementierungen dieses Interfaces und ruft zum gegebenen Zeitpunkt die drei Methoden auf, die im Interface definiert sind. Die Methode OnApplicationStarted ist die passende für unsere Zwecke.

using Unity;
using Unity.AspNet.Mvc;
using Unity.Injection;
using Unity.RegistrationByConvention;
class UnityEvents : IApplicationEventHandler
{
    public void OnApplicationStarted(
        UmbracoApplicationBase httpApplication,
        ApplicationContext applicationContext
    )
    {
        var container = UnityConfig.Container;
 
        // Web API
        GlobalConfiguration.Configuration.DependencyResolver
            = new Unity.AspNet.WebApi.UnityDependencyResolver( container );
        // MVC
        DependencyResolver.SetResolver( new Unity.AspNet.Mvc.UnityDependencyResolver( container ) );
 
        // The UmbracoContext must be registered so that the Umbraco backoffice controllers 
        // can be successfully resolved.
        container.RegisterType<UmbracoContext>(
            new PerRequestLifetimeManager(),
            new InjectionFactory( c => UmbracoContext.Current )
        ).RegisterType<ApplicationContext>(
            new PerRequestLifetimeManager(),
            new InjectionFactory( c => ApplicationContext.Current )
        ).RegisterType<RenderMvcController>(
            new InjectionConstructor( new ResolvedParameter<UmbracoContext>() ) );
 
        var umbracoControllers = AllClasses.FromAssemblies( typeof( UmbracoApplication ).Assembly ).Where( c => typeof( ApiController ).IsAssignableFrom( c ) );
 
        container.RegisterTypes( umbracoControllers, null, null, null, t => new[] { new InjectionConstructor() } );
    }
 
    public void OnApplicationInitialized( UmbracoApplicationBase httpApplication, ApplicationContext applicationContext ) { }
 
    public void OnApplicationStarting( UmbracoApplicationBase httpApplication, ApplicationContext applicationContext ) { }
}

Die erste Zeile bemüht die Klasse UnityConfig, um von dieser einen Containter zu erhalten. UnityConfig.cs wurde beim Laden der Unity-Packages erzeugt und legt den Container an.

using System;
using Unity;
using Unity.RegistrationByConvention;
 
namespace UmbracoUnity
{
    /// <summary>
    /// Specifies the Unity configuration for the main container.
    /// </summary>
    public static class UnityConfig
    {
        #region Unity Container
        private static Lazy<IUnityContainer> container =
          new Lazy<IUnityContainer>(() =>
          {
              var container = new UnityContainer();
              RegisterTypes(container);
              return container;
          });
 
        /// <summary>
        /// Configured Unity Container.
        /// </summary>
        public static IUnityContainer Container => container.Value;
        #endregion
 
        /// <summary>
        /// Registers the type mappings with the Unity container.
        /// </summary>
        /// <param name="container">The unity container to configure.</param>
        /// <remarks>
        /// There is no need to register concrete types such as controllers or
        /// API controllers (unless you want to change the defaults), as Unity
        /// allows resolving a concrete type even if it was not previously
        /// registered.
        /// </remarks>
        public static void RegisterTypes(IUnityContainer container)
        {
            // This will automatically scan an assembly for classes fitting this convention:
            // Interface name: IMyClass
            // Class name: MyClass
            // and automatically register those types for you.
            // If you do decide to use this feature, make sure it is the first one called.
            // Conflicting registrations that follow will override previous ones.
            container.RegisterTypes(
                AllClasses.FromAssemblies( typeof(UnityConfig).Assembly ),
                WithMappings.FromMatchingInterface,
                WithName.Default
            );
        }
    }
}

Dies geschieht mit einer Lazy-Instanzierung (siehe die Klasse Lazy), die nach unserer Einschätzung eigentlich nicht viel bringt, weil die Instanz immer unmittelbar nach dem Applikations-Start abgerufen wird. Dennoch lassen wir den Code, wie er ist.

Das erste, was in UnityConfig mit dem neu angelegten Container passiert, ist der Aufruf der Methode RegisterTypes. Hier ist der richtige Ort für Zuordnungen zwischen Interfaces und ihre Implementierungen.

Eigene Services registrieren

Dabei kann man einer Konvention folgen: wenn es im gegenwärtigen Assembly ein Interface IMyClass und eine Klasse MyClass gibt, dann werden diese automatisch registriert. Das sieht man in dem obenstehenden Code. Die Magie geschieht durch WithMappings.FromMatchingInterface.

Wir müssen unsere Controller (Web API und MVC) nicht explizit registrieren, da diese automatisch registriert werden, sofern sie sich im gleichen Assembly befinden, wie die UnityConfig-Klasse.

Für die Registrierung von Services, die außerhalb des gegenwärtigen Assemblies liegen, ist ein Pattern ratsam, bei dem ein bestimmtes Interface (sagen wir IModuleRegistration) in jedem Assembly implementiert wird, das an der Dependency Injection teilnehmen will.

public interface IModuleRegistration
{
    void RegisterTypes(IUnityContainer container);
}
public class OneOfMyModulesRegistration : IModuleRegistration
{
    public void RegisterTypes( IUnityContainer container )
    {
        container.RegisterTypes(
            AllClasses.FromAssemblies( typeof(OneOfMyModulesRegistration).Assembly ),
            WithMappings.FromMatchingInterface,
            WithName.Default
        );
 
        // und / oder
 
        var umbracoControllers = AllClasses.FromAssemblies( typeof( OneOfMyModulesRegistration ).Assembly ).Where( c => typeof( ApiController ).IsAssignableFrom( c ) );
        container.RegisterTypes( umbracoControllers, null, null, null, t => new[] { new InjectionConstructor() } );
 
    }
}

In RegisterTypes des Moduls werden zuerst alle Interface/Implementierungs-Paare registriert, wie es bereits in UnityConfig geschehen ist. Danach werden alle von ApiController abgeleiteten Klassen gesucht und als Controller registriert.

In UnityConfig.RegisterTypes können nun alle Implementierungen des Interfaces IModulteRegistration gesucht und eine Instanz des zugrundeliegenden Typs angelegt werden. Danach wird die Methode RegisterTypes des Interfaces aufgerufen. 

Wenn nun jedes Modul eine solche Registrierungsklasse aufweist, hat man ein wunderbares Plug-and-Play-System. Das ist jedoch in diesem Beispiel nicht nötig, weshalb wir zurückkehren können zur Klasse UnityEvents.

Das System auf Dependency Injection umstellen

Wir müssen nun dem ASP.NET MVC- und dem ASP.NET Web API-System sagen, dass eine DependencyInjection stattfindet. Dafür gibt es Adapterklassen, die das Interface IDependencyResolver implementieren. Die Details, wie das geschieht, müssen uns nicht interessieren. Wir melden einfach unsere Resolver an, einmal für ASP.NET MVC und einmal für Web API.

ASP.NET MVC hat eine andere Art und Weise, einen solchen Resolver anzumelden, als Web API, weshalb hier einmal der Resolver in der globalen Konfiguration angemeldet wird und im Fall von MVC die statische Instanz mit DependencyResolver.SetResolver gesetzt wird.

Nun geht es an die Registrierung von Services, die von Umbraco kommen. UmbracoContext und ApplicationContext sind Instanzen, die an den HTTP-Request gekoppelt sind. Sie brauchen ein Lifetime-Management, das letztlich dafür sorgt, dass die Referenz auf die Objekte aufgelöst wird, sobald der Request beendet ist. Das macht es möglich, die Objekte über die Garbage Collection zu entsorgen.

Dann müssen wir alle Controller von Umbraco registrieren, weil diese jetzt nicht direkt, sondern über Unity angelegt werden. Wir verschaffen uns erst einmal eine Liste dieser Controller (in der Zeile mit AllClasses.FromAssemblies…) und können diese dann in einer weiteren Codezeile allesamt registrieren.

Ein Anwendungsbeispiel

Drei Klassen: das ist alles, was nötig ist, um DependencyInjection mit Unity in ein Umbraco-Projekt zu integrieren. Jetzt müssen wir das natürlich noch testen. Wir legen dazu eine absolut simple Interface/Implementierungs-Kombination an:

public class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
public interface ICustomerFactory { Customer CurrentCustomer { get; } }
public class CustomerFactory : ICustomerFactory { public Customer CurrentCustomer { get => new Customer() { FirstName = "Mirko", LastName = "Matytschak" }; } }

In der Praxis werden die Implementierungen vielleicht etwas komplexer ausfallen. Aber für die Illustration der DependencyInjection reicht das erst einmal aus.

Nun wollen wir einen API Controller schreiben, der die CustomerFactory verwendet. Das geschieht folgendermaßen:

public class CustomerController : UmbracoApiController
{
    private readonly ICustomerFactory customerFactory;
 
    public CustomerController(ICustomerFactory customerFactory)
    {
        this.customerFactory = customerFactory;
    }
 
    [HttpGet]
    public object GetCurrent()
    {
        var customer = this.customerFactory.CurrentCustomer;
        return new { firstName = customer.FirstName, lastName = customer.LastName };
    }
}

Beachten Sie den Konstruktor. In einem System ohne DependencyInjection würden wir eine Exception bekommen, weil ein Controller einen Konstruktor ohne Parameter braucht. Mit DependencyInjection jedoch funktioniert der Code, der den Controller anlegt, etwas anders. Er sieht, dass wir einen Konstruktor mit dem Parameter ICustomerFactory haben. Der Code, der übrigens im DependencyResolver liegt, versucht nun, aus dem Container ein Objekt mit dem Interface ICustomerFactory zu ziehen.

Der DependencyResolver erkennt, dass die Implementierung CustomerFactory für das Interface ICustomerFactory registriert ist. Hätte CustomerFactory jetzt ebenfalls einen Konstruktor mit Parametern, würde der Resolver versuchen, auch diese Parameter zur Verfügung zu stellen. Nach dem Anlegen der CustomerFactory übergibt der Resolver die Factory an den Konstruktor des Controllers und dieser kann nun die Factory benutzen.

Aufruf des Api-Controllers

Der Code funktioniert und ist mit dem Aufruf /umbraco/api/customer/getcurrent testbar. Um die Funktionalität des Controllers in Unit Tests zu überprüfen, müssen wir noch einen Schritt weitergehen und die Tätigkeiten des Controllers an eine neue Klasse delegieren:

public class CustomerControllerHandler
{
    private readonly ICustomerFactory customerFactory;
 
    public CustomerControllerHandler(ICustomerFactory factory)
    {
        this.customerFactory = factory;
    }
 
    public object GetCurrent()
    {
        var customer = this.customerFactory.CurrentCustomer;
        return new { firstName = customer.FirstName, lastName = customer.LastName };
    }
}
 

Der Controller tut dann nichts selbst, sondern delegiert alle Tätigkeiten:

public class CustomerController : UmbracoApiController
{        
    private CustomerControllerHandler customerControllerHandler;
 
    public CustomerController(ICustomerFactory customerFactory)
    {
        this.customerControllerHandler = new CustomerControllerHandler( customerFactory );
    }
 
    [HttpGet]
    public object GetCurrent()
    {
        return this.customerControllerHandler.GetCurrent();
    }
}

Warum tun wir das? Die Erklärung dafür liegt darin, dass wir in einem Testprojekt nicht ohne größere Aufwände einen Controller anlegen können, um seine Methoden für diverse Tests auszuführen. Statt des Controllers können wir in den Tests jetzt den ControllerHandler anlegen und die Testinstanzen unserer Interfaces an diesen übergeben.

Statt jetzt die CustomerFactory an den CustomerController zu übergeben, könnten wir auch ein Interface ICustomerControllerHandler definieren, was zur automatischen Registrierung des CustomerControllerHandlers in Unity führen würde. Dann könnten wir diesen im Konstruktor an den Controller übergeben und müssten ihn nicht selbst anlegen. Das würde sich vor allem dann empfehlen, wenn der Handler noch weitere Abhängigkeiten hat. Für die Tests ergibt sich daraus kaum ein Unterschied.

MVC-Controller mit Unity anlegen

Als nächstes Beispiel möchten wir zeigen, wie ein MVC-Controller mit Unity erzeugt werden kann. Dazu legen Sie im Backoffice Ihrer Umbraco-Instanz einen neuen Dokumenttyp UnityDocument an. Lassen Sie Umbraco einen View dafür erzeugen und tragen Sie dort etwas Testcode ein. Für unsere Tests habe ich ein Property angelegt, einen RichText-Editor mit dem Alias „text“. Der Testcode gibt den Text dann mit @Model.Content.GetPropertyValue( "text" ) aus.

Legen Sie dann eine Seite von diesem Dokumenttyp an. Rufen Sie die Seite auf. Ihr Testcode sollte erscheinen. Wenn nicht, dann müssen eventuell noch ein paar Kleinigkeiten geändert werden, die nicht im Zusammenhang mit DependencyInjection stehen.

Wird die Seite angezeigt, können Sie nun einen Controller für den Dokumenttypen anlegen. Das geschieht nach einer Namenskonvention. Der Controller muss heißen wie der Dokumenttyp und er muss von RenderMvcController abgeleitet werden.

public class UnityDocumentController : RenderMvcController
{
    private readonly ICustomerFactory customerFactory;
 
    public UnityDocumentController(ICustomerFactory customerFactory)
    {
        this.customerFactory = customerFactory;
    }
 
    public override ActionResult Index( RenderModel model )
    {
        TempData.Add( "Customer", this.customerFactory.CurrentCustomer );
        return View(model);
    }
}

Die Logik ist exakt dieselbe, wie beim Api-Controller: Wir brauchen einen Konstruktor, der unsere Abhängigkeiten auflistet. Diese werden beim Anlegen des Controllers automatisch zur Verfügung gestellt. In der Methode Index wird dann die Factory verwendet, um ein Customer-Objekt in die Temporärdaten zu legen. Der View kann diese dann auslesen:

@using Umbraco.Web.Mvc;
@using UmbracoUnity.Classes;
 
@inherits UmbracoTemplatePage
@{
    Layout = null;
    var customer = (Customer)TempData["Customer"];
}
 
@Model.Content.GetPropertyValue( "text" )
 
@if (customer != null)
{
    <p>Current Customer:</p>
    <p>@customer.FirstName @customer.LastName</p>
}
else
{
    <p>No Customer</p>
}

Auch hier ergibt sich die Testbarkeit dadurch, dass der Controller seine gesamte Logik an einen Handler delegiert, wie wir es bereits beim Web API gezeigt haben.

Fazit

Dependency Injection ist unverzichtbar, um die Logik von Web-Anwendungen mit Unit Tests testbar zu machen. ASP.NET MVC und die Web API-Implementierung von Microsoft kommt bereits mit Vorkehrungen zur Einbindung von Lösungen zur DependencyInjection. Wir haben anhand des Beitrags gezeigt, dass die Einbindung von Unity einfach und schnell zu bewerkstelligen ist. Damit steht den Unit Tests in Umbraco nichts mehr im Weg.

Einen Kommentar verfassen

Anleitung zur Textformatierung

Zum Formatieren des Textes verwenden Sie [b][/b] und [i][/i]. Verwenden Sie [url=http://ihre-site]Text[/url] für Links.

* Pflichtfelder