ASP.NET MVC und WebAPI über Dependency Injection Instanziieren (Castle Windsor) – Part 2


Wie ich bereits in meinem ersten Artikel zur Serie TDD erwähnt habe, verwende ich das Dependency Injection (DI) Framework Castle Windsor mit ASP.NET MVC um meine Controller entsprechend zu instanziieren.

Bei der Anwendung von DI mit ASP.NET MVC muss man unterscheiden zwischen den Standard Controllern (DefaultControllerFactory) und den WebAPI Controllern (IHttpControllerActivator). Daher ist für den jeweiligen Controller-Typ ist eine eigene Implementierung notwendig.

Was macht das DI Framework eigentlich für uns?

Anhand von Regeln, die man dem DI Container hinterlegt, kann das DI Framework selbstständig Instanzen von Klassen erstellen. Aufgrund dessen, gibt es im Programm nur noch eine zentrale Stelle an der Instanzen von Klassen erstellt werden. Auch hier gibt es Ausnahmen, dazu gehören z.B. einfache Datenhaltungsklassen. Nichtdestotrotz muss jeweils anhand des vorliegenden Problems geklärt werden, ob eine Instanz über DI übergeben werden muss oder evtl. lokal instanziiert werden kann.

In einem ASP.NET MVC Projekt befindet sich die zentrale Stelle für das Einbinden von Klasseninstanzen beim Erstellen eines Controllers. Da der Standard Controller aber parameterlos ist, muss im jeweiligen Controller einfach ein Konstruktor mit den gewünschten Parametern erstellt werden. Damit dieser Konstruktor dann auch aufgerufen und mit den passenden Abhängigkeiten instanziiert wird, muss das Erstellen der Controller von uns überschrieben werden. Das Instanziieren übernimmt dann Castle Windsor für uns, mit den von uns hinterlegten Regeln.

1. ASP.NET MVC DI mit Castle Windsor initialisieren

Wie bereits erwähnt, benötigen wir einen Castle Windsor Container indem wir unsere Regeln für das Instanziieren von Klassen, z.B. Controllern ablegen. Das Ganze muss in der Global.asax im “Application_Start” entsprechend aufgerufen werden.

private static IWindsorContainer Container { get; set; }

protected void Application_Start()
{
    ...
    //Erstellen unserer Castle Windsor Instanz die wir im gesamten Projekt benutzten
    Container = new WindsorContainer();
    //Hinzufügen unserer Konfigurationen zum Kontainer.
    Container.Install(new WebInstaller(connectionString), new LoggingInstaller());
    ...
}

Über den so genannten Installer von Castle Windsor können wir unsere einzelnen Regelpakete für den Container hinzufügen (registrieren). Der WebInstaller sieht z.B in Auszügen folgendermaßen aus und bezieht sich auf eines meiner Projekte. Wie die Regeln genau aussehen können und was zu beachten ist, kann man auch sehr gut in der Castle Windsor Dokumentation nachlesen.

///
/// Installer für alle wichtigen Webverweise
///
public class WebInstaller : IWindsorInstaller
{
    private  string ConnectionString { get; set; }

    ///
    /// Initialize WebInstaller
    ///
    /// Connectionstring übergeben, der für den Webinstaller benötigt wird.
    public WebInstaller(string connectionString)
    {
        ConnectionString = connectionString;
    }

    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        //Unseren EFModelContainer registrieren mit dem passenden Connectionstring dazu, ACHTUNG wir müssen die bereits erstellte Instanz verwenden,
        //da wir darauf auch bei einigen anderen statischen "Variablen" zugreifen, z.B. Rechteverwaltung und Co.
        container.Register(Component.For(typeof(MainModelContainer)).Instance(CurrentHttpContext.GetDataModel()).LifestylePerWebRequest());

        //Alle Queries Registrieren die sich in der Assembly "User.EF.Queries" befinden und jeweils ein passendes Interface enthalten wie
        //"IMemberQueries/MemberQueries" siehe auch: http://docs.castleproject.org/Windsor.Registering-components-by-conventions.ashx
         container.Register(Classes.FromAssemblyContaining().InNamespace("User.EF.Queries").WithService.DefaultInterfaces().LifestylePerWebRequest());

        //Alle Controller Registrieren die IController beinhalten/nutzen.
        container.Register(Classes.FromAssemblyContaining().BasedOn().LifestylePerWebRequest());
        //Alle WebAPI Controller verwenden IHttpController
        container.Register(Classes.FromAssemblyContaining().BasedOn().LifestylePerWebRequest());
        ....
      }
}

Nach dem Castle Windsor soweit vorbereitet ist und unseren Controller entsprechend instanziieren kann, benötigen wir unsere eigene ControllerFactory. Dafür muss eine neue Klasse erstellt werden, die von “DefaultControllerFactory” ableitet und der wir unseren Castle Windsor Container in einem eigenen Konstruktor übergeben können. Mit Hilfe von “this._container.Resolve(controllerType)” entscheidet Castle Windsor selbstständig welcher Controller aufgerufen wird und welche Abhängigkeiten erstellt sowie instanziiert werden müssen.

 public class CustomControllerFactory : DefaultControllerFactory
 {
     private readonly IWindsorContainer _container;

     ///
     /// Den Windsor Container für unser Projekt übergeben
     ///
     /// Unser aktueller Windsor Kontainer
     public CustomControllerFactory(IWindsorContainer container)
     {
         if (container == null)
         {
             throw new NullReferenceException("IWindsorContainer is Null");
         }

         _container = container;
     }

     ///
     /// Erstellen der passenden Controller Instanzen anhand unseres Containers werden die Instanzen automatisch aufgelöst.
     ///
     protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
     {
         if (controllerType == null)
         {
             throw new HttpException(404, string.Format("The controller for path '{0}' could not be found.", requestContext.HttpContext.Request.Path));
         }

         //Unseren Controller auflösen per Castle Windsor
         var controller = (IController)this._container.Resolve(controllerType);

         return controller;
     }

     public override void ReleaseController(IController controller)
     {
         //Wenn der Controller "aufgelöst werden soll, dann auch den Controller aus dem Context entfernen.
         if (HttpContext.Current.Items.Contains(CurrentControllerIndexName))
         {
             HttpContext.Current.Items.Remove(CurrentControllerIndexName);
         }

         this._container.Release(controller);
     }
 }

Damit unsere neue Factory zum Erstellen von Controllern auch von ASP.NET MVC “gefunden” bzw. versendet werden kann, muss diese noch registriert werden. Dies geschieht auch in der global.asax im “Application_Start”

//Erstellen unserer CustomControllerFactory zum Instanziieren der Passenden Controller
var customControllerFactory = new CustomControllerFactory(Container);
//Unsere CustomControllerFactory als Standard Factory zum Erstellen von Controllern setzten, damit diese auch von MVC verwendet wird.
ControllerBuilder.Current.SetControllerFactory(customControllerFactory);

Wenn soweit alles eingestellt ist, kann ein MVC Controller z.B. auch folgenden Konstruktor besitzen, denn durch Castle Windsor werden alle Abhängigkeiten entsprechend aufgelöst und instanziiert übergeben.

 [CustomAuthorize]
 public class AccountController : ControllerBaseUser
 {
     #region Member
     ///
     /// Unser Repository für die Mitarbeiterabfragen
     ///
     private readonly IDiMembership _membership;

     ///
     /// User Repository für alle allgemeinen Abfragen fürs Web.
     ///
     private readonly IWebQuerySummary _webQueries;

     ///
     /// Mailverwaltung für die Accountverwaltung
     ///
     private readonly IAccountMails _accountMails;
     #endregion

     #region Initialize
     public AccountController(IDiMembership membership, IAccountMails accountMails, IWebQuerySummary webQueries)
     {
         if (membership == null)
         {
             throw new NullReferenceException("IDiMembership is Null");
         }

         if (accountMails == null)
         {
             throw new NullReferenceException("AccountMails is Null");
         }

         if (webQueries == null)
         {
             throw new NullReferenceException("WebQuery is Null");
         }

         _webQueries = webQueries;
         _accountMails = accountMails;
         _membership = membership;
     }
     #endregion

2. ASP.NET WebAPI mit Castle Windsor initialisieren

Leider funktioniert die DI der Standard Controller nicht bei den WebAPI Controllern. Damit auch die WebAPI Controller mit DI instanziiert werden können sind die folgenden Schritte notwendig.

Dabei kann natürlich auch in der WebAPI auf die gleichen DI Regeln wie bei den Standard Controllern zurückgegriffen werden. Wir müssen nur im WebInstaller aufpassen, dass wir auch eine Regel für die WebAPI Controller angeben, damit diese aufgelöst werden können.

//Alle WebAPI Controller verwenden IHttpController
container.Register(Classes.FromAssemblyContaining().BasedOn().LifestylePerWebRequest());

Dann kann auch der bereits angelegte Container für die Standard Controller auch für die WebAPI verwendet werden.

Die WebAPI benötigt eine eigene Controller Klasse, der wir unseren DI Container übergeben können und in der dann die WebAPI Controller entsprechend aufgelöst werden können.

///
/// Um die webApi auch über IoC nutzen zu können ist folgender Code notwendig.
/// Und dann muss das ganze über "GlobalConfiguration.Configuration" in der Global Assax registriert werden.
/// http://www.cafe-encounter.net/p1316/windsor-di-with-asp-net-webapi
///
public class CustomWebApiController : IHttpControllerActivator
{
    private readonly IWindsorContainer _container;

    public CustomWebApiController(IWindsorContainer container)
    {
        this._container = container;
    }

    public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
    {
        var controller = (IHttpController)this._container.Resolve(controllerType);
        request.RegisterForDispose(new Release(() => this._container.Release(controller)));
        return controller;
    }

    private class Release : IDisposable
    {
        private readonly Action release;

        public Release(Action release)
        {
            this.release = release;
        }

        public void Dispose()
        {
            this.release();
        }
    }
}

Jetzt muss, wie bereits bei den Standard Controllern, unsere neue Klasse nur noch registriert werden, so dass sie die WebAPI Controller Instanzen anlegen kann. Dazu wird wieder in der Global.asax in der “Application_Start” die entsprechende Codezeile eingefügt.

GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator), new CustomWebApiController(Container));

Und schon kann unser WebAPI Controller den folgenden Konstruktor enthalten.

 public class AdminApiController : ApiController
 {
     #region Member
     ///
     /// User Repository für alle allgemeinen Abfragen fürs Web.
     ///
     private readonly IWebQuerySummary _webQueries;
     #endregion

     #region Initialize
     public AdminApiController(IWebQuerySummary webQueries)
     {
         if (webQueries == null)
         {
             throw new NullReferenceException("WebQuery is Null");
         }

         _webQueries = webQueries;
     }
     #endregion
}

CodePlex

Mein bereits angefangenes Projekt findet Ihr wie immer unter Codeplex –> DiTemplates –> UserManagement

Quellen

http://www.cafe-encounter.net/p1316/windsor-di-with-asp-net-webapi

http://www.devtrends.co.uk/blog/how-not-to-do-dependency-injection-the-static-or-singleton-container

http://docs.castleproject.org/Windsor.Registering-components-by-conventions.ashx

Advertisements

Ein Gedanke zu „ASP.NET MVC und WebAPI über Dependency Injection Instanziieren (Castle Windsor) – Part 2

  1. Pingback: ASP.NET MVC und Dependency Injection für Views – Part 3 | SquadWuschel's Blog

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s