SignalR-Hubs mit Nutzername und Passwort absichern

Sehr häufig sollen nicht alle Anwender eines Systems berechtigt sein alle, durch SignalR-Hubs bereitgestellten Funktionen zu nutzen. Im Folgenden wird gezeigt, wie man eine Benutzeranmeldung mit Username und Password für einen SignalR-Hub selbst implementieren kann.

Das AuthorizeAttribute

Die AuthorizeAttribute Klasse ist Bestandteil von SignalR und dient dazu Zugriffe auf einen Hub einzuschränken. Da sie von der Attribute-Klasse erbt, kann man ihre Objekte wie ein „Tag“ an andere Klassen (z.B. einen SignalR-Hub) „anheften“. SignalR verwendet dieses AuthorizeAttribute-Objekt dann um herauszufingen, ob ein Zugriff auf den Hub berechtigt ist, oder nicht.

Wir erstellen nun eine neue Klasse namens HubAuthorizationAttribute, welche von AuthorizeAttribute erbt, und implementieren darin unsere Funktionen zur Passwort-Überprüfung. Anschließend werden wir ein Objekt dieser Klasse als Attribut an einen SignalR-Hub anheften.

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

    public class HubAuthorizationAttribute : AuthorizeAttribute
    {
        public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
        {
            string user = request.Headers["X-User"];
            string passwd = request.Headers["X-PW"];
        
            // Hier prüfen, ob user und passwd gültig sind!
            // Wenn der Zugriff erlaubt ist, true zurückgeben. Andernfalls false.

            return true; // In diesem Fall: Zugriff erlaubt.
        }

        public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
        {
            return true;
        }
    }

Die Methode AuthorizeHubConnection wird immer aufgerufen, wenn ein Client sich mit einem Hub verbinden möchte. Gibt diese Methode ein true zurück, ist die Verbindung erlaubt. Gibt die Methode dagegen ein false zurück, wird die Verbindung abgewiesen. Im obigen Beispiel-Code gehen wir davon aus, dass der Client seinen Nutzernamen und Password in die Felder X-User und X-PW des Http-Headers der Anfrage geschrieben hat. Da wir beim Aufruf von AuthorizeConnection den Http-Request als Parameter übergeben bekommen, haben wir Zugriff auf diese Felder. Im Beispiel oben kopieren wir den Nutzernamen und das Password in die String-Variablen user und passwd. Wir können uns nun z.B. mit einer Benutzerdatenbank verbinden und prüfen, ob die Login-Daten gültig sind. Im Beispiel oben haben wir diesen Schritt einfach ausgelassen und geben immer ein true zurück, denn es soll hier lediglich das Prinzip erklärt werden.

Einen SignalR-Hub absichern

Wir können nun einen SignalR-Hub absichern, indem wir unser neues Attribut HubAuthorization an den Hub anheften, d.h. wir schreiben es in eckigen Klammern vor die Hub-Klasse (hier: ExampleHub):

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
 
namespace MyWebApplication
{
    [HubAuthorization]
    [HubName("ExampleHub")]
    public class ExampleHub : Hub
    { 
        public ExampleHub()
        {
        }
 
        public int Add(int a, int b)
        {
            return a+b;
        }
    }
}

Immer wenn nun eine Verbindung zu diesem Hub hergestellt werden soll, wird diese  von dem HubAuthorizationAttribute geprüft und ggf. abgelehnt.

User und Password durch einen C# Client übermitteln

Das folgende Beispiel zeigt, wie die Felder X-User und X-PW in einem C# Client in den Http-Header eingetragen werden können:

var hubConnection = new HubConnection("http://localhost:57256/");
var proxy = hubConnection.CreateHubProxy("ExampleHub");
hubConnection.Headers["X-User"] = "Beispielnutzer";
hubConnection.Headers["X-PW"] = "geheim";

hubConnection.Start().Wait();
Console.WriteLine(proxy.Invoke<int>("Add", new object[] { 2,3} ).Result); // Ergebnis: 5

Datenaustausch zwischen Client und Server in C# mit SignalR

Bei der Entwicklung von Anwendungen bestehend aus einem Server und einem oder mehreren Clients tritt folgender Ablauf sehr häufig auf:

  1. Ein Client fordert über die API des Servers einen Datensatz an.
  2. Der Server holt diesen Datensatz aus z.B. einer Datenbank.
  3. Der Server entfernt aus diesem Datensatz alle Werte, die für den Client nicht relevant sind oder (z.B. aus Datenschutzgründen) nicht an den Client übertragen werden sollen. Oder er kopiert nur die für den Client relevanten Daten aus dem Datensatz in eine neue Datenstruktur.
  4. Der Datensatz oder die neue Datenstruktur wird an den Client gesendet.
  5. Der Client bzw. sein Anwender ändert einige Werte des Datensatzes und sendet ihn zurück an den Server.
  6. Der Server muss nun den Datensatz aus der Datenbank und die vom Client gesendeten Daten zusammenführen und wieder in der Datenbank speichern. Dabei sind alle Werte, welche vom Client nicht zurück gesendet wurden (also den Wert null haben) nicht zwangsläufig vom Client geändert worden, sondern wurden möglicherweise bereits vorher vom Server entfernt. Diese dürfen natürlich nun nicht auch in der Datenbank gelöscht werden.

Dem Client soll also nur eine Untermenge der tatsächlich in einem Datensatz befindlichen Daten bekannt sein. Das hat zudem den Vorteil, dass eine Erweiterung des Datensatzes in neueren Versionen des Servers keine Auswirkungen auf den Client hat(sofern dieser nicht auch mit den neuen Daten arbeiten soll).

Im Folgenden wird ein generischer Ansatz vorgestellt werden, der eine nachträgliche  Änderung der Datenstrukturen mit minimalen Aufwand ermöglicht.

Unsere Beispielanwendung

Gehen wir davon aus, wir möchten die Mitarbeiter einer Firma in einer Datenbank verwalten. Die Webapplikation speichert von jedem Mitarbeiter eine Id, den Vor- und Nachnamen, seine Abteilung, sein Geburtsdatum und sein Gehalt. Es soll eine Client-Anwendung geben, welche sich via SignalR mit der Webapplikation auf dem Server verbindet und die Mitarbeiter-Datensätze herunterladen kann. Der Anwender des Clients kann einen Mitarbeiter nun bearbeiten, z.B. seine Abteilung ändern. Anschließend werden diese Änderungen wieder an die Webapplikation übertragen und in der Datenbank gespeichert. Aus Gründen des Datenschutzes, dürfen die Daten für Geburtstag und Gehalt jedoch nicht mit an den Client übertragen werden. Der Client kennt also nur einen Teil der über einen Mitarbeiter gespeicherten Daten. Vor dem Übertragen eines Mitarbeiter-Datensatzes an den Client müssen also die Vertraulichen Daten entfernt werden. Nach dem Zurücksenden eines Datensatzes vom Client zum Server muss dieser mit den vertraulichen Daten wieder zusammengeführt werden.

Webapplikation, Client und eine gemeinsame DLL

Um das Problem elegant zu lösen, erstellen wir zunächst drei Projekte: Die Webapplikation, eine Client-Anwendung (z.B. für den Desktop) und eine DLL, welche von den beiden anderen Projekten referenziert wird.

In der Webapplikation erstellen wir eine Klasse namens Employee, welche alle Daten über einen Mitarbeiter speichert und z.B. mit dem Entity Framework auf eine Datenbanktabelle abgebildet wird. In der DLL erstellen wir eine Klasse namens EmployeeSubset. Diese Klasse enthält alle Daten eines Mitarbeiters, welche auch dem Client bekannt sein sollen. Für die Kommunikation zwischen Server und Client verwenden wir nun ausschließlich die Klasse EmployeeSubset.

Der Server muss also alle Employee-Objekte aus der Datenbank in EmployeeSubset-Objekte umwandeln können und EmployeeSubset-Objekte vom Client mit den dazu gehörenden Employee-Objekten aus der Datenbank zusammenführen können.

Auf den ersten Blick liegt es Nahe, zwischen den Klassen Employee und EmployeeSubset eine Vererbung einzurichten. In diesem Fall könnten Employee-Objekte einfach als EmployeeSubset gecastet werden. Aus folgenden Gründen führt dies jedoch nicht zum Ziel:

  • Nur weil ein Objekt auf einen anderen Typ gecastet wird, ändert sich dadurch nicht der Typ des Objekts selbst. SignalR wird das Objekt weiterhin als Employee-Objekt erkennen und den kompletten Datensatz zum Client übertragen.
  • In einer Anwendung mit mehreren unterschiedlichen Clients, die alle eine andere Subset-Klasse benötigen, müsste die Employee-Klasse von allen diesen Klassen erben. In C# kann jedoch immer nur eine Klasse an eine andere Klasse vererben.
  • Zum Zusammenführen von Employee-Objeken und EmployeeSubset-Objekten würde uns eine Vererbung keine Vorteile bringen.

Objekte durch Reflection umwandeln und zusammenführen

Im Folgendem sehen wir einen Ansatz, wie man die Umwandlung zwischen den Klassen Employee und EmployeeSubset mittels Reflection durchführen können. Die Methode CreateSubset (welche eine Extension Method ist und daher in einer static Klasse stehen muß) sucht nach allen schreibbaren Properties in der Subset-Klasse T und kopiert den Inhalt einer gleichnamigen Property aus dem als source-Parameter übergebenen Objekt in das Subset-Objekt.
Die Methode ImportSubset sucht alle lesbaren Properties in dem Subset-Parameter. Wenn eine Property mit dem gleichen Namen in dem target

        public static T CreateSubset<T>(this object source) where T : new()
        {
            T subset = new T();
            Type srcType = source.GetType();
            foreach (PropertyInfo p in typeof(T).GetProperties().Where(p => p.CanWrite))
            {
                PropertyInfo sp = srcType.GetProperty(p.Name);
                if (sp == null) throw new Exception("The property '" + p.Name + "' of type '" + typeof(T).FullName + "' does not exist in type '" + srcType.FullName + "'!");
                p.SetValue(subset, sp.GetValue(source));
            }
            return subset;
        }

        public static void ImportSubset(this object target, object subset)
        {
            Type tgtType = target.GetType();
            foreach (PropertyInfo p in subset.GetType().GetProperties().Where(p => p.CanRead))
            {
                PropertyInfo tp = tgtType.GetProperty(p.Name);
                if (tp == null) throw new Exception("The property '" + p.Name + "' of type '" + subset.GetType().FullName + "' does not exist in type '" + tgtType.FullName + "'!");
                tp.SetValue(target, p.GetValue(subset));
            }
        }

Wir können diese Methode also wie folgt verwenden:

            Employee employee = new Employee() { // Beispiel-Employee erstellen
                Id = 1,
                Forename = "Max",
                Surname = "Muster",
                Department = "Entwicklung",
                Birthday = new DateTime(1990, 6, 5),
                Salary = 65000
            };

            // Erstellen eines Subsets durch den Server
            EmployeeSubset subset = employee.CreateSubset<EmployeeSubset>();  

            // Hier würde das Subset z.B. durch SignalR an den Client übertragen werden.

            // Der Client verändert das Subset:
            subset.Department = "Vertrieb";

            // Hier würde das geänderte Subset zurück an den Server gesendet.

            // Der Server führt den ursprünglichen Datensatz mit dem Subset zusammen
            employee.ImportSubset(subset);

            // employee.Department hat nun ebenfalls den Wert "Vertrieb"

Die Vorteile dieses Vorgehens:

  • Das Erstellen und Zusammenführen des Subsets erfordert nur noch je eine Zeile Programmcode.
  • Die Datensätze Employee und EmployeeSubset lassen sich mit sehr wenig Aufwand erweitern: Es reicht dazu aus, einfach neue Properties in die Klassen einzufügen. Wenn in beiden Klassen neue Properties mit gleichem Namen und Typ eingefügt werden, werden diese automatisch bei der Subset-Erstellung und dem Zusammenführen berücksichtigt.

Subversion: Neue Revisionsnummer automatisch im Quelltext aktualisieren

Die Versionsverwaltung Subversion bzw. SVN ist unter Softwareentwickern allgemein bekannt. Auf dem Windows-Desktop wird sie meist in Form von TortoiseSVN eingesetzt, welches eine gute Integration in die Windows-Umbebung bietet.

Zum Verwalten der Versionen eines Projekts verwendet Subversion fortlaufende Revisionsnummern. Um Fehler in bereits freigegebenen Softwareständen zu untersuchen, ist es oft sehr hilfreich, wenn man weiß aus welcher Quelltext-Revision die jeweilige Software gebaut wurde. Ideal wäre es, wenn das Programm seine eigene Revisionsnummer z.B. in der Titelleiste oder einem Info-Dialog anzeigen würde.

Diese Anleitung zeigt, wie man die Revisionsnummer mit TortoiseSVN automatisch in einer Quelltextdatei aktualisieren lassen kann.

Vorbereitung

  1. Erstellen Sie ein neues SVN-Repository.
  2. Erstellen Sie ein neues Arbeitsverzeichnis auf ihrem PC und checken Sie das Repository darin aus. (Rechtsklick in den Ordner > SVN Checkout)
  3. Erstellen Sie mit dem VisualStudio ein neues beliebiges Projekt in dem Ordner.

Revisionsnummer in Datei einfügen

Wir werden nun eine neue Datei namens Revision.cs erstellen, in welcher die Revisionsnummer automatisch aktualisiert wird. Wird diese Datei anschließend zu einem Projekt hinzugefügt, ist die Nummer dann als Konstante zur Laufzeit im Programm verfügbar.

Erstellen Sie zunächst eine Datei namens Revision.cs.tmpl im obersten Verzeichnis des Arbeitsverzeichnisses (nicht in einem Projekt-Order) mit einem Texteditor und fügen Sie diesen Quelltext ein:

using System;

public static class Revision
{
    private const int REVISION = $WCREV$;
    
    public static int Get()
    {
        return REVISION;
    }
}

Öffnen Sie nun eine Konsole (Start > Ausführen > cmd.exe) und wechseln Sie in Ihr Arbeitsverzeichnis (cd \pfad\zum\verzeichnis ). Geben Sie nun folgenden Befehl ein:

SubWCRev . Revision.cs.tmpl Revision.cs

Subversion hat nun basierend auf Ihrer Template-Datei Revision.cs.tmlp eine neue Datei namens Revision.cs erstellt. In dieser Datei wurde das $WCREV$ ausgetauscht durch die aktuelle Versionsnummer. Diese Datei können wir nun im VisualStudio zu unserem Projekt hinzufügen:

  1. Öffnen Sie das VisualStudio.
  2. Klicken Sie mit der rechten Maustaste im Solution Explorer auf das Projekt und wählen Sie Add > Existing Item und wählen Sie die Datei Revision.cs aus.
  3. Klicken Sie NICHT auf Add, sondern auf den kleinen Pfeil neben dem Button und wählen Sie Add as Link. Wenn Sie stattdessen auf Add klicken, wird eine Kopie der Revision.cs im Projektverzeichnis erstellt, in der die Revisionsnummer nicht aktualisiert wird. Daher: Add as Link.

Gehen wir nun davon aus, bei Ihrer Anwendung handelt es sich um eine Konsolenanwendung, welche beim Start ihre Revisionsnummer auf dem Bildschirm anzeigen soll. Dies erreichen Sie in dem Sie die Datei Program.cs wie folgt anpassen:

namespace MyConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Revisionsnummer: " + Revision.Get());
            Console.ReadLine();
        }
    }
}

Wir können nun jederzeit durch den Aufruf von SubWCRev (siehe oben) unsere Revision.cs aktualisieren lassen. Damit die Revisionsnummer in der Datei immer aktuell ist, müssen wir also dafür sorgen, dass SubWCRev vor jedem Build oder nach jedem SVN Commit oder SVN Update automatisch aufgerufen wird.

Automatisieren mit TortoiseSVN

In den Einstellungen von TortoiseSVN können wir den automatischen Aufruf nach einem Commit/Update einrichten. Erstellen Sie zunächst eine neue Datei namens update-rev.bat im gleichen Verzeichnis wie die Revision.cs.tmpl mit folgendem Inhalt:

cd %1
del Revision.cs
SubWCRev . Revision.cs.tmpl Revision.cs

Beim Aufruf dieser Datei wechselt das Skript zunächst in das als Parameter übergebene Verzeichnis (welches das Arbeitsverzeichnis sein muss) und löscht die alte Revision.cs. Anschließend wird eine neue aktuelle Revision.cs erzeugt.

TortoiseSVN soll diese Datei nun bei jeder Revisionsänderung aufrufen. Dies richten Sie wie folgt ein:

  1. Klicken Sie mit der rechten Maustaste in das Arbeitsverzeichnis im Windows-Explorer.
  2. Wählen Sie im PopUp-Menü TortoiseSVN > Settings.
  3. In dem erscheinenden Fenster wählen Sie auf der linken Seite Hook Scripts
  4. Klicken Sie auf Add um ein neues Hook Skript hinzuzufügen.
  5. In dem neuen Fenster wählen Sie als Hook Type: Post Update Hook und geben sie in das Feld Working Copy Path den Pfad zu Ihrem Arbeitsverzeichnis ein (z.B. „C:\pfad\zum\arbeitsverzeichnis“)
  6. In dem Feld Command Line To Execute tragen Sie den vollständigen Pfad zur update-rev.bat gefolgt von dem Pfad zum Arbeitsverzeichnis als Parameter. Also z.B. „C:\pfad\zum\arbeitsverzeichnis\update-rev.bat C:\pfad\zum\arbeitsverzeichnis“. Bestätigen Sie im OK.
  7. Legen Sie noch ein neues Hook Skript an und wählen Sie als Hook Type: Post Commit Hook. Tragen Sie in die Felder Working Copy Path und Command Line To Execute genau den gleichen Inhalt wie bei dem vorherigen Skript ein.

Die Datei updaterev.bat wird nun von TortouseSVN automatisch nach jedem Update und jedem Commit aufgerufen und bekommt dabei als Parameter das Arbeitsverzeichnis. Mit cd %1 wechselt sie in das Arbeitsverzeichnis, da %1 für den ersten Parameter des Skripts steht. Anschließend wird die Revision.cs aktualisiert.

 

Erstellen einer ASP.NET MVC Webapplikation mit Castle Windsor, Entity Framework und SignalR inklusive SignalR-Client

Dieser Beitrag ist eine kurze allgemeine Anleitung zum Erstellen einer MVC Webapplikation mit folgenden Komponenten:

  • Eine SQL-Datenbankanbindung mit dem Entity Framework
  • Dependency Injection mit Castle Windsor
  • Einer SignalR-API über die Daten mit einem Client ausgetauscht werden können

Weiterhin wird in dieser Anleitung ein minimaler .NET Client für die SingalR-API entwickelt.

Anlegen eines neuen Projekts in Visual Studio 2017

  1. Wählen Sie File > New > Project im Menü und wählen Sie ASP.NET Web Application (.NET Framework) aus dem Ordner Visual C# / Web als Projekttemplate aus. Geben Sie einen Namen für die Webapplikation ein z.B. „MyWebApplication“ und klicken Sie auf OK.
  2. Im folgenden Fenster wählen Sie MVC als Projekttyp und klicken Sie OK.

Installieren von Castle Windsor

  1. Klicken Sie im Solution Explorer mit der rechten Maustaste auf das Projekt und wählen Sie Manage NuGet Packages im erscheinenden PopUp-Menü.
  2. Suchen Sie im Tab Browse nach dem Paket Castle.Windsor und installieren Sie dieses.

Um während der weiteren Installation überprüfen zu können, ob Castle Windsor funktioniert erstellen wir zunächst eine Beispiel-Klasse sowie ein Interface, welches wir anschließend in Castle Windsor registrieren und verwenden können. Unsere Beispiel-Klasse stellt zwei Methoden zum Addieren und Subtrahieren zweier Zahlen zur Verfügung.
Erstellen Sie zwei neue Dateien namens „ExampleDependency.cs“ und „IExampleDependency.cs“ mit folgendem Inhalt:

IExampleDependency.cs

namespace MyWebApplication
{
  public interface IExampleDependency
  {
    int add(int a, int b);
    int subtract(int a, int b);
  }
}

ExampleDependency.cs

namespace MyWebApplication
{
  public class ExampleDependency :IExampleDependency
  {
    public int add(int a, int b)
    {
      return a + b;
    }

    public int subtract(int a, int b)
    {
      return a - b;
    }
  }
}

Als nächstes erstellen wir eine Installer-Klasse für Castle Windsor, in der alle Abhängigkeiten registriert werden. Fügen Sie die folgende neue Datei samt Inhalt zum Projekt hinzu:

WindsorInstaller.cs

using Castle.MicroKernel.Registration;
using Castle.MicroKernel.SubSystems.Configuration;
using Castle.Windsor;
using System.Web.Mvc;

namespace MyWebApplication
{
    public class WindsorInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(Classes.FromThisAssembly().BasedOn<IController>().LifestyleTransient());
            container.Register(Component.For<IExampleDependency>().ImplementedBy<ExampleDependency>());
        }
    }
}

Die Klasse WindsorInstaller registriert alle Klassen, die von IController erben. Bei einem eingehenden Webrequest kann das zu dessen Verarbeitung erforderliche MVC-Controller-Objekt im Folgenden ebenfalls durch Castle Windsor erzeugt werden. Weiterhin wird unsere Klasse ExampleDependency als Singleton-Klasse registriert. Sie wird also nur bei der ersten Verwendung einmalig instanziiert. Sie können weitere Abhängigkeiten nach dem gleichen Schema hinzufügen.

Damit das Erzeugen der MVC-Controller-Objekte durch Castle Windsor erfolgt, müssen wir noch eine neue Controller-Factory schreiben, welche dazu einen Windsor-Container verwendet. Fügen Sie die dazu folgende neue Datei samt Inhalt zum Projekt hinzu:

WindsorControllerFactory.cs

using Castle.Windsor;
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace MyWebApplication
{
    public class WindsorControllerFactory : DefaultControllerFactory
    {
        WindsorContainer container;

        public WindsorControllerFactory(WindsorContainer container)
        {
            this.container = container;
        }

        public override void ReleaseController(IController controller)
        {
            container.Kernel.ReleaseComponent(controller);
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType == null)
            {
                throw new HttpException(404, "The controller for path " + requestContext.HttpContext.Request.Path + " could not be found.");
            }
            return (IController)container.Kernel.Resolve(controllerType);
        }
    }
}

Unsere Anwendung soll einen zentralen Windsor-Container zum Erstellen aller Objekte verwenden. In diesem Controller sollen beim Start der Anwendung alle Abhängigkeiten aus unserer Klasse WindsorInstaller registriert werden.
Dazu müssen Sie die Datei Global.asax.cs wie folgt bearbeiten:

  • Legen Sie eine neue Klassenvariable namens „container“ vom Typ WindsorContainer an.
  • In der Methode Application_Start erstellen Sie einen neuen WindsorContainer und führen unseren WindsorInstaller aus. (Siehe dazu die Kommentare unten im Quelltext)
  • Damit die Controller von unserer neuen Controller-Factory erzeugt werden, registrieren Sie diese im ControllerBuilder.
  • Legen Sie die Methode Application_End an, sofern diese noch nicht existiert, und rufen Sie darin die Dispose-Methode des Windsor-Containers auf.

Anschließend sieht Ihre Global.asax.cs z.B. wie folgt aus:

using Castle.Windsor;
using Castle.Windsor.Installer;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace MyWebApplication
{
    public class MvcApplication : System.Web.HttpApplication
    {
        public static WindsorContainer container;

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            container = new WindsorContainer(); // Neuen WindsorContainer erstellen
            container.Install(FromAssembly.This()); // Unseren WindsorInstaller ausführen
            //container.Install(FromAssembly.Named("Library")); // Bei Bedarf: Weitere WindsorInstaller aus z.B. zusätzlichen DLLs ausführen.

            ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(container)); // Neue Factory für Controller registrieren
        }

        protected void Application_End()
        {
            container.Dispose();
        }
    }
}

Funktionstest
Testen wir nun, ob die Dependency Injection durch Castle Windsor in Ihrer neuen Applikation funktioniert. Ersetzen Sie den Inhalt der Datei HomeController.cs durch folgenden Quelltext:

using System.Web.Mvc;

namespace MyWebApplication.Controllers
{
    public class HomeController : Controller
    {
        IExampleDependency calculator;

        public HomeController(IExampleDependency calculator)
        {
            this.calculator = calculator;
        }


        public ActionResult Index()
        {
            return Content("Addieren zweier Zahlen: 5 + 3  = " + calculator.add(5, 3));
        }

    }
}

Wenn Sie nun die Webapplikation starten und mit dem Webbrowser aufrufen, sehen Sie folgenden Text im Webbrowser:

Addieren zweier Zahlen: 5 + 3 = 8

Perfekt! Alles funktioniert.

Zum besseren Verständnis erkläre ich noch einmal was genau beim Laden der Webseite passiert ist:

Der Webbrowser sendet eine Http-Anfrage an unsere Applikation. Um diese zu bearbeiten benötigt ASP.NET einen neuen HomeController, welcher durch unsere neue WindsorControllerFactory erstellt werden muss. Unsere WindsorControllerFactory beauftragt den WindsorContainer, welcher beim Start der Applikation angelegt wurde, damit das Objekt zu erstellen. Der WindsorContainer erkennt, dass zum Erstellen eines HomeControllers ein Objekt, welches das Interface IExampleDependency implementiert, vom Konstruktor benötigt wird. Durch den WindsorInstaller haben Sie vorgegeben, dass hierfür ein Objekt der Klasse ExampleDependency verwendet werden soll. Dieses Objekt wird ebenfalls vom WindsorContainer erstellt und der fertige HomeController zurück gegeben, welcher die Http-Anfrage verarbeitet.

Bei einer erneuten Http-Anfrage wird der WindsorContainer kein neues ExampleDependency-Objekt erstellen, da bereits ein Objekt dieser Klasse existiert und diese Klasse mit LifeStyleSingleton() im WindsorInstaller als Singleton-Klasse markiert wurde.

Installieren des Entity Frameworks

Im nächsten Schritt möchten wir in unserer Applikation eine Datenbank verwenden und dafür das Entity Framework benutzen:

  1. Klicken Sie im Solution Explorer mit der rechten Maustaste auf das Projekt und wählen Sie Manage NuGet Packages im erscheinenden PopUp-Menü.
  2. Suchen Sie im Tab Browse nach dem Paket EntityFramework und installieren Sie dieses.

Wir werden nun die Anwendung dahingehend erweitern, dass sie Angestellte einer Firma verwalten kann. Dazu soll sie Objekte vom Typ Employee in einer Datenbank speichern und daraus laden können. Fügen Sie die Klasse Employee zu Ihrem Projekt hinzu:

Employee.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace MyWebApplication
{
    public class Employee
    {
        [Key]
        public int Id { get; set; }
        public string Forename { get; set; }
        public string Surname { get; set; }
        public string Department {get; set;}
        public DateTime Birthday { get; set; }
        public decimal Salary { get; set; }
    }
}

Erstellen Sie nun eine DbContext-Klasse. Aus dieser Klasse entnimmt das Entity Framework, welche Tabellen in der Datenbank existieren sollen. In unserem Beispiel möchten wir nur eine Tabelle haben, deren Spalten den Properties aus der Klasse Employee entsprechen. Mit dem Attribut [Key] in der Employee-Klasse legen wir fest, dass Id der PrimaryKey in der Datenbanktabelle sein soll. Fügen Sie die folgende neue Datei zum Projekt hinzu:

AppDbContext.cs

using System.Data.Entity;

namespace MyWebApplication
{
    public class AppDbContext :DbContext
    {
        public AppDbContext() : base("Persist Security Info=False;Integrated Security=true;Initial Catalog=MyWebApplication;server=localhost")
        {
        }

        public DbSet<Employee> Employees { get; set; }
    }
}

Der ConnectionString, welcher mit base() an den Konstruktor von DbContext übergeben wird, legt die Verbindungseinstellungen mit der Datenbank fest. Damit wir unsere AppDbContext-Objeke von Castle Windsor verwalten lassen können, müssen wir die Klasse zu unserem WindsorInstaller hinzufügen. Fügen Sie dazu die Zeile 8 des folgenden Listings zur Klasse WindsorInstaller hinzu.

    public class WindsorInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(Classes.FromThisAssembly().BasedOn<IController>().LifestyleTransient());
            container.Register(Component.For<IExampleDependency>().ImplementedBy<ExampleDependency>().LifestyleSingleton());

            container.Register(Component.For<AppDbContext>().LifestylePerWebRequest());
        }
    }

Zum Testen ändern Sie die Klasse HomeController wie folgt:

    public class HomeController : Controller
    {
        IExampleDependency calculator;
        AppDbContext ctx;

        public HomeController(IExampleDependency calculator, AppDbContext ctx)
        {
            this.calculator = calculator;
            this.ctx = ctx;
        }


        public ActionResult Index()
        {
            return Content("Addieren zweier Zahlen: 5 + 3  = " + calculator.add(5, 3));
        }


        public ActionResult AddEmployees()
        {
            ctx.Employees.Add(new Employee()
            {
                Forename = "Max",
                Surname = "Mustermann",
                Department = "Entwicklungsabteilung",
                Birthday = new DateTime(1986, 4, 1),
                Salary = 65000
            });

            ctx.Employees.Add(new Employee()
            {
                Forename = "Bernd",
                Surname = "Beispiel",
                Department = "Verwaltung",
                Birthday = new DateTime(1988, 5, 10),
                Salary = 40000
            });

            ctx.SaveChanges();
            return Content("Mitarbeiter wurden gespeichert!");
        }
    }
}

Wenn Sie nun die Methode AddEmployees im Webbrowser aufrufen, wird Ihre Webapplikation sich mit der Datenbank verbinden, die Tabelle anlegen und zwei neue Mitarbeiter darin speichern. Sie können dies z.B. mit dem Microsoft SQL-Server Management Studio überprüfen.

Installation von SignalR mit Dependency Injection durch Castle Windsor

Unsere Webapplikation soll weiterhin eine SignalR-API bereitstellen, über die Clients mit der Applikation Daten austauschen können.

  1. Klicken Sie im Solution Explorer mit der rechten Maustaste auf das Projekt und wählen Sie Manage NuGet Packages im erscheinenden PopUp-Menü.
  2. Suchen Sie im Tab Browse nach dem Paket Microsoft.AspNet.SignalR und installieren Sie dieses.

Bevor wir einen SignalR-Hub zu unserer Applikation hinzufügen, werden wir (analog zu der ControllerFactory für MVC-Controller) auch eine neue Factory für SignalR-Hubs schreiben, damit wir die Hubs ebenfalls durch Castle Windsor erzeugen lassen können. Erstellen Sie dazu eine neue Klasse namens SignalrHubFactory mit folgendem Inhalt:

SignalrHubFactory.cs

using Castle.Windsor;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.SignalR;

namespace MyWebApplication
{
    public class SignalrHubFactory : DefaultDependencyResolver
    {
        WindsorContainer container;
        public SignalrHubFactory(WindsorContainer container)
        {
            this.container = container;
        }
        public override object GetService(Type serviceType)
        {
            return container.Kernel.HasComponent(serviceType) ? container.Resolve(serviceType) : base.GetService(serviceType);
        }
        public override IEnumerable<object> GetServices(Type serviceType)
        {
            return container.Kernel.HasComponent(serviceType) ? container.ResolveAll(serviceType).Cast<object>() : base.GetServices(serviceType);
        }
    }
}

Die SignalrHubFactory bekommt in ihrem Konstruktor eine Referenz auf den WindsorContainer. Sobald ein neuer Hub von ihr erstellt werden soll, versucht sie es zunächst über den WindsorContainer zu beziehen. Falls die gewünschte Klasse dort nicht verfügbar ist, wird sie mit dem DefaultDependencyResolver von SignalR erstellt.

Damit Castle Windsor alle Hubs für uns erstellen kann, müssen die Hub-Klassen noch im WindsorInstaller registriert werden. Fügen Sie dazu die Zeile 6 aus dem folgenden Listing in Ihre Klasse WindsorInstaller ein.

    public class WindsorInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, IConfigurationStore store)
        {
            container.Register(Classes.FromThisAssembly().BasedOn<IController>().LifestyleTransient());
            container.Register(Classes.FromThisAssembly().BasedOn<Microsoft.AspNet.SignalR.Hub>().LifestyleTransient());
            container.Register(Component.For<IExampleDependency>().ImplementedBy<ExampleDependency>().LifestyleSingleton());

            container.Register(Component.For<AppDbContext>().LifestylePerWebRequest());
        }
    }

Nun erstellen Sie eine OWIN-Startup-Klasse, in der Sie die neue SignalrHubFactory als Resolver für SignalR-Hubs festlegen:
Startup.cs

using Microsoft.AspNet.SignalR;
using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(MyWebApplication.Startup))]

namespace MyWebApplication
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var factory = new SignalrHubFactory(MvcApplication.container);
            var cfg = new HubConfiguration() { Resolver = factory, EnableDetailedErrors = true };
            app.MapSignalR(cfg);
        }
    }
}

Abschließend erstellen wir noch als Beispiel einen SignalR-Hub, welcher zwei Funktionen zum addieren und subtrahieren zweier Zahlen bereitstellt. Der Hub berechnet die Summen bzw. Differenzen nicht selbst, sondern greift dazu auf unsere ExampleDependency zurück, welche von Castle Windsor über den Konstruktor des Hubs bereit gestellt wird. Erstellen Sie eine neue Klasse namens ExampleHub.cs und fügen Sie den folgenden Inhalt darin ein:
ExampleHub.cs

using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace MyWebApplication
{
    [HubName("ExampleHub")]
    public class ExampleHub : Hub
    {
        IExampleDependency calculator;

        public ExampleHub(IExampleDependency calculator)
        {
            this.calculator = calculator;
        }

        public int Add(int a, int b)
        {
            return calculator.add(a, b);
        }

        public int Subtract(int a, int b)
        {
            return calculator.subtract(a, b);
        }
    }
}

Programmieren eines SignalR-Clients

Zum Testen unserer SignalR-Schnittstelle, werden wir eine einfache Client-Anwendung programmieren. Dazu erstellen wir eine neue Applikation für die Konsole, welche sich beim Start mit der API verbindet und mit ihrer Hilfe zwei Zahlen subtrahiert.

  1. Wählen Sie File > New > Project im Menü und wählen Sie Console App (.NET Framework) aus dem Ordner Visual C# als Projekttemplate aus. Geben Sie einen Namen für die Webapplikation ein z.B. „MyClient“ ein.
  2. Wählen Sie unten unter Solution „Add to Solution“ aus und klicken Sie auf OK.
  3. Klicken Sie im Solution Explorer mit der rechten Maustaste auf das Projekt und wählen Sie Manage NuGet Packages im erscheinenden PopUp-Menü.
  4. Suchen Sie im Tab Browse nach dem Paket Microsoft.AspNet.SignalR.Client und installieren Sie dieses.

Öffnen Sie die Datei Program.cs und fügen Sie folgenden Inhalt ein:

using Microsoft.AspNet.SignalR.Client;
using System;

namespace MyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            using (HubConnection hubConnection = new HubConnection("http://localhost:57256/"))
            {
                IHubProxy proxy = hubConnection.CreateHubProxy("ExampleHub");
                hubConnection.Start().Wait();

                int d = proxy.Invoke<int>("Subtract", 10, 3).Result;
                Console.WriteLine("Subtrahieren zweier Zahlen: 10 - 3 = " + d);
            }
            Console.ReadLine();
        }
    }
}

Vergessen Sie nicht die Portnummer in Zeile 10 durch die Portnummer, auf der Ihre Webapplikation läuft, zu ersetzen.

Klicken Sie nun mit der rechten Maustaste im Solution Explorer auf Ihre Solution und wählen Sie im PopUp-Menü Set Startup Projects. Wählen Sie Multiple Startup Projects und stellen Sie beide Projekte (die Webapplikation und den Client) auf Start.

Wenn Sie nun Ihre Solution starten, werden beide Projekte ausgeführt. Die Webapplikation erscheint wie gehabt im Browserfenster. Der Client erscheint als Konsolenfenster und zeigt folgenden Text an:

Subtrahieren zweier Zahlen: 10 – 3 = 7

Im Folgenden soll der Ablauf noch einmal erklärt werden: Beim Start erstellt der Client einen Proxy, über den wir auf die Methoden des ExampleHubs zugreifen können. Durch den Aufruf von proxy.Invoke() rufen wir eine Methode auf dem Server auf (im Beispiel die Methode Subtract). Dazu wird zunächst eine Instanz des ExampleHubs benötigt, welche von unserer SignalrHubFactory mit dem WindsorContainer erstellt wird. Castle Windsor erkennt, dass zum erstellen eines ExampleHubs eine ExampleDependency benötigt wird, und stellt auch diese automatisch bereit.

Den volständigen Beispiel-Quellcode können Sie hier herunterladen: MyWebApplication.zip