Das Backend stellt die Server-Funktionalität für das AdventureWorks Cinema Beispiel zur Verfügung. Ziel dieser Backend-Implementierung ist zu zeigen, wie eine mehrschichtige Anwendung mit einer Web Service-Schnittstelle komfortabel kommunizieren und rollenbasierte Sicherheit mit Hilfe der in .NET vorhandenen Konzepte dennoch umgesetzt werden kann. Dabei werden mehrere Schichten verwendet, die auch unabhängig voneinander eingesetzt werden können. Das Backend wurde vollständig in C# programmiert. Auf dieser Seite
Aufbau des SamplesAuch der Applikationsserver ist in mehrere Schichten eingeteilt. Die Web Service Schicht stellt die Serverfunktionalität rechnerübergreifend auch anderen Maschinen und Betriebsystemen zur Verfügung, enthält aber selbst keine eigentliche Logik. Die eigentliche Geschäftslogik wird von der Server Geschäftslogik implementiert. Die Aufgabe der Datenzugriffsschicht ist es, von der konkreten Datenbank zu abstrahieren, um die Geschäftslogik nicht an eine bestimmte Datenbank zu binden. Um die Datenbank auszutauschen reicht es also, die Datenzugriffsschicht gegen eine andere Version auszutauschen. Als Datenbank für das Backend wird eine SQL Server 2005 Express Datenbank verwendet. Diese ist kostenlos, auch für den kommerziellen Einsatz, und trotz der Beschränkungen der Express Edition für die AdventureWorks Cinema Anwendung mehr als ausreichend leistungsfähig.
Technologien/KonzepteWeb Service
Geschäftslogik
Datenzugriffsschicht
SQL Server
Datenhaltung und -zugriffDie Datenzugriffsschicht (Data Access Layer) wird von der Geschäftslogik verwendet, um komfortabel auf die Datenbank zuzugreifen. Dabei ist es Aufgabe der Datenzugriffsschicht, das Laden der Daten vorzunehmen und so genannte Data Transfer Objects (DTO) daraus zu erstellen. Außerdem ist die Datenzugriffsschicht dafür verantwortlich, DTOs in die Datenbank zu schreiben. Übersicht über die Architektur der DatenzugriffsschichtDie Einordnung der Datenzugriffsschicht in das Gesamtbild zeigt folgende Abbildung:
Die DatenbankDatenbankschema Die Beispielanwendung ist darauf ausgelegt, die Erstellung und Modifikation von Daten ohne Verbindung zum Server zu erlauben (Offline Modus). Aus diesem Grund muss jeder Client in der Lage sein, eine eindeutige ID zu erstellen, um Dubletten zu vermeiden, die Synchronisationsprobleme hervorrufen könnten. Anders gesagt: die ID der Entitäten (der Primärschlüssel) muss weltweit eindeutig sein. Um dies zu erreichen wurde eine GUID (in der Datenbank uniqueidentifier genannt) als Datentyp für alle Primärschlüssel verwendet, da sie genau diese Funktionalität bietet. Alle Tabellen in der Cinema Datenbank und ihre Beziehungen:
Die Verwendung von gespeicherten Prozeduren Gespeicherte Prozeduren werden nur dazu verwendet, Daten in die Datenbank zu schreiben, also beim Erstellen, Ändern und Löschen von Datensätzen. Interessant ist dabei die Befüllung der Intersektionstabellen (z.B. Users_Roles), die im selben Schritt wie das Erstellen/Aktualisieren der Benutzerdaten erfolgen soll. Dazu müssen den gespeicherten Prozeduren CreateUser/UpdateUser alle Rollen übergeben werden, denen der Benutzer angehört. Das Problem an dieser Stelle ist jedoch, dass es nicht möglich ist, Arrays oder andere Sammlungen an die Datenbank zu übergeben. Dank den neuen XML Möglichkeiten des neuen SQL Server 2005 ist dies jedoch kein Problem mehr. Im C# Teil der Datenzugriffsschicht wird einfach ein XML Dokument erstellt, das mit dem text Datentyp an die Datenbank übergeben und von dieser entsprechend behandelt wird. Es wurden keine gespeicherten Prozeduren für das Abrufen der Daten erstellt, da der C# Teil der Datenzugriffsschicht die zurückgegebenen Daten auch zu Objekten konvertieren muss und der Quellcode leichter zu verstehen und warten ist, wenn die Abfragen und der Konvertierungs-Code an einer Stelle gehalten werden. Der Quellcode der DatenzugriffsschichtÖffentliche Schnittstelle Die Datenzugriffsschicht ist in mehrere statische Klassen eingeteilt, eine Klasse pro Entität. Jede Klasse hat das Suffix "Dal". Diese Klassen werden verwendet, um die Funktionalität nach Entität zu gruppieren, z.B. wird die CountryDal-Klasse verwendet, um Länder zu erstellen, aktualisieren, löschen und alle Länder abzurufen. Implementierungsdetails Eine eigene Klasse mit dem Namen ExceptionBuilder wird verwendet, um alle Ausnahmen zu erzeugen, die geworfen werden. Dies ermöglicht die Protokollierung aller Ausnahmen auf einfache Art und Weise. Die ExceptionBuilder-Klasse enthält dabei eine eigene Methode für jeden auftretenden Ausnahmezustand (z.B. ein Argument ist NULL, ein Argument ist ungültig oder die Kommunikation mit der Datenbank ist fehlgeschlagen). Der Nachteil dieses Ansatzes ist, dass FxCop die Parameternamen nicht mehr überprüfen kann, die den Konstruktoren von ArgumentNullException oder ArgumentException übergeben werden. Alle Klassen der Datenzugriffsschicht verwenden die statische Klasse SqlHelper, die die Verwaltung von Datenbankverbindungen und Transaktionen kapselt. Diese verlangt, dass Parameter, die an eine gespeicherte Prozedur oder eine Abfrage übergeben werden sollen, als IDictionary<string, object> übergeben werden. Der Grund dafür ist, dass einige Werte konvertiert werden müssen, bevor sie der Datenbank übergeben werden können. Z.B. muss der Wert NULL als System.DBNull.Value übergeben werden. Der Nachteil hierbei ist, dass nur Eingabeparameter an die Datenbank übergeben werden können, aber keine Ausgabe-, Ein-/Ausgabe- oder Rückgabeparameter. Da diese Parameterrichtungen jedoch nicht benötigt werden ist dies eine akzeptable Einschränkung. GeschäftslogikDie Geschäftslogik-Schicht des Servers enthält die Geschäftslogik des AdventureWorks Cinema Samples. Einige Methoden der Geschäftslogik bestehen aus einfachen Aufrufen der Datenzugriffsschicht, andere enthalten zusätzliche Geschäftsregeln und führen mehrere Operationen auf der Datenzugriffsschicht aus. Dabei wird mithilfe von rollenbasierter Sicherheit geprüft, ob der aktuell angemeldete Benutzer überhaupt berechtigt ist, bestimmte Aktionen mit der Geschäftslogik auszuführen. Wichtig für die rollenbasierte Sicherheit sind hier insbesondere die Klassen CinemaPrincipal und CinemaIdentity. Anhand dieser Klassen wird gezeigt, wie man eigene Implementierungen der IPrincipal und IIdentity Schnittstellen schreiben und für rollenbasierte Sicherheit verwenden kann. Darf eine Methode nur von bestimmten Benutzern aufgerufen werden, so kann dies durch Verwendung des PrincipalPermission-Attributs sichergestellt werden:
[PrincipalPermission(SecurityAction.Demand,
Role = CinemaPrincipal.EmployeeRoleName)]
public static void CreateNewsItem(NewsItem newsItem)
{
NewsDal.CreateNewsItem(newsItem);
}
Im Web Service wird ein neuer CinemaPrincipal erzeugt und an die Thread.CurrentPrincipal-Eigenschaft zugewiesen. Bei der Erzeugung des CinemaPrincipal wird diesem ein User-Objekt des aktuell angemeldeten Benutzers übergeben. Beim Aufruf einer Methode die mit einer PrincipalPermission gekennzeichnet ist, prüft das .NET Framework über die IsInRole()-Methode aus dem IPrincipal-Interface, ob der Principal zu einer Gruppe ("Role") gehört, die in der PrincipalPermission gefordert wird. Da der CinemaPrincipal ein User-Objekt enthält, kann er über dieses in der IsInRole()-Methode prüfen, ob der Benutzer Mitglied der angeforderten Gruppe ist. Nur wenn diese Prüfung erfolgreich ist, wird der Methodenaufruf ausgeführt, ansonsten wird eine SecurityException geworfen. Web ServiceDie Clients kommunizieren mit dem Backend über einen ASP .NET Web Service. Für den Zugriff auf diesen Web Service verwenden alle Clients eine gemeinsame Komponente, den Service Access Layer. Der AdventureWorks Cinema Web Service soll zeige, wie man "Tickets" zur Authentifizierung der Clients beim Web Service verwenden kann. Außerdem zeigt das Sample, wie man einen Web Service "contract first", d.h. ausgehend von einer Schnittstellenbeschreibung des Web Services (im Gegensatz zu "code first", wo man den Code zuerst schreibt) entwickeln kann. AuthentifizierungDer Web Service authentifiziert Clients über "Tickets". Nach der Anmeldung am Web Service über die LogOn-Methode wird ein benutzerdefinierter SOAP-Header erzeugt, der in der weiteren Kommunikation automatisch zwischen dem Client und dem Server übertragen wird. Dieser Header enthält eine Id (ein "Ticket"), über das alle weiteren Aufrufe einer bestimmten Sitzung und somit der Benutzerkennung, welche in der LogOn-Methode angegeben wurde, zugeordnet werden kann. Durch das SoapHeader-Attribut mit "Direction=SoapHeaderDirection.Out" der LogOn-Methode wird festgelegt, dass diese Methode einen SoapHeader vom Typ SessionHeader an den Client überträgt. In der LogOn-Methode wird überprüft, ob Benutzerkennung und Kennwort korrekt sind, und anschließend eine neue Sitzung erzeugt. Die Sitzung enthält einen Verweis auf den Benutzer und die IP-Adresse des Clients. Die Id dieser Sitzung wird im SessionHeader gespeichert:
[WebMethod]
[SoapHeader("SessionHeader", Direction = SoapHeaderDirection.Out)]
public LogOnResponse LogOn(LogOnRequest LogOnRequestMessage)
{
LogOnResponse response = new LogOnResponse();
try
{
response.User = UserManager.LogOn(
LogOnRequestMessage.UserName, LogOnRequestMessage.Password);
Session session = new Session();
session.User = response.User;
session.Id = Guid.NewGuid();
session.IPAddress = Context.Request.UserHostAddress;
session.StartDate = DateTime.Now;
SessionManager.CreateSession(session);
SessionHeader = new SessionHeader();
SessionHeader.SessionId = session.Id;
}
catch (Exception ex)
{
LogException(ex);
}
return response;
}
Methoden, die nur von bestimmten Benutzern ausgeführt werden sollen, können den Methodenaufruf mit der Id aus dem SessionHeader einer Sitzung und somit einem Benutzer zuordnen. Durch das SoapHeader-Attribut mit "Direction = SoapHeaderDirection.In" wird der SessionHeader, den der Server in der LogOn Methode an den Client geschickt hat, vom Client an den Server übertragen. Anschließend wird in der EnsureSessionIsValid()-Methode geprüft, ob es zu der SessionId aus dem SessionHeader eine gültige Session in der Datenbank gibt, und ob die IP-Adresse des Clients mit der IP-Adresse welche die Session erzeugt hat übereinstimmt.
[WebMethod]
[SoapHeader("SessionHeader", Direction = SoapHeaderDirection.In)]
public void DeleteUser(DeleteUserRequest DeleteUserRequestMessage)
{
EnsureSessionIsValid();
// weiterer Code.
}
Falls die Id aus dem SessionHeader in der EnsureSessionIsValid()-Methode einem Benutzer zugeordnet werden konnte, wird ein neues CinemaPrincipal-Objekt angelegt und dem aktuellen Thread zugewiesen. Dies ermöglicht die Verwendung von rollenbasierter Sicherheit in der Geschäftslogik-Schicht des Backends. Thread.CurrentPrincipal = new CinemaPrincipal(session); "contract first" Web Service EntwicklungBeim "contract first" Ansatz wird zuerst ein Dienstvertrag (contract) für den Web Service definiert. Aus diesem wird anschließend der Code generiert. Alle Nachrichten, die der Web Service verschicken und empfangen kann wurden zuerst in XML Schema Dateien definiert. Aus diesen werden die Data Transfer Objects (DTOs) und Nachrichtenklassen generiert, die als Datentypen für die Web Service Operationen verwendet werden. Die Generierung des Codes aus den XSD Dateien erfolgt mit dem Tool Xsd.exe aus dem .NET Framework SDK. DownloadVoraussetzungen Installieren Sie Visual Studio 2005 inklusive der Programmiersprache C# und SQL Server 2005 Express. Installation
Die in die Solution integrierte Datenbank enthält bereits einige Testdaten, die zusätzlich auch in der mit dem Client ausgelieferten SampleData.xml zu finden sind:
Hier erreichen Sie unser Downloadcenter, um das Sample herunterzuladen. |