heise Developer

Know-how 25.01.2012 - 07:31

Holger Schwichtenberg

Von der Datenbank bis zur Oberfläche mit .NET, Teil 1: Datenzugriff und Logik

Schöner fliegen

.NET rühmt sich der durchgängigen Programmiersprachen und -techniken vom Server bis zum Client. In vielen Beispielen sieht man aber oft nur einzelne umgesetzt. Eine fünfteilige Artikelserie zeigt, wie in einem mehrschichtigen Projekt verschiedene .NET-Techniken unterschiedlich zusammenarbeiten können.

.NET rühmt sich der durchgängigen Programmiersprachen und -techniken vom Server bis zum Client. In vielen Beispielen sieht man aber oft nur einzelne umgesetzt. In einer fünfteiligen Artikelserie zeigt heise Developer, wie in einem mehrschichtigen Projekt verschiedene .NET-Techniken unterschiedlich zusammenarbeiten können. Der Teil 1 beginnt mit dem Datenzugriff und der Geschäftslogik.

Der Fokus des Fallbeispiels soll auf dem Zusammenspiel der Techniken liegen, nicht auf dem Geschäftsprozess. Letzterer ist daher bewusst überschaubar gewählt: Aufgabe ist es, eine Buchungsmaske für Flüge aus der Sicht eines Call-Center-Mitarbeiters zu implementieren. Dabei wählt der Anwender einen Passagier sowie einen Flug und löst die Buchung aus. Die Auswahl kann jeweils über die Eingabe einer ID erfolgen oder über eine Suche nach Namen (bei Passagieren) beziehungsweise Flugstrecke (bei Flügen). Preisunterschiede, Rückflüge, Mitreisende, Vielfliegerkarten und viele andere alltägliche Fälle des Flugverkehrs bleiben unberücksichtigt. Auch Authentifizierung soll kein Thema sein. Angelehnt ist das Tutorial an die Beispielanwendung von World Wide Wings [1], einer fiktiven Fluggesellschaft, die alle Anwendung mit .NET erstellt.

Von der Datenbank bis zur Oberfläche mit .NET

  • Teil 1: Datenzugriff und Logik
  • Teil 2: Webservices [2]
  • Teil 3: Webanwendung mit ASP.NET [3]
  • Teil 4: Desktop-Anwendung mit WPF [4]
  • Teil 5: Desktop- und Browseranwendung mit Silverlight [5]

Um das Beispiel mitzuprogrammieren, benötigt der Entwickler Grundkenntnisse in der C#-Syntax und der Handhabung von Visual Studio 2010. Eingesetzt werden sollte Visual Studio 2010 Professional oder höher. Damit ist auch .NET 4.0 auf dem Entwicklungssystem vorhanden. Grundsätzlich könnte man mit den kostenfreien Express-Versionen ebenfalls zum Ziel kommen, das Handhaben der Projekte wäre aber aufwendiger und würde mehr Komplexität in das Beispiel bringen. Wer nicht mitprogrammieren will, kann sich das Ergebnis jedes einzelnen Teils hier [6] herunterladen.

Das Tutorial verwendet die englische Version von Visual Studio, weil es in der deutschen Ausgabe einen Fehler gibt, durch den das Tutorial-Beispiel nicht ohne viel manuelle Arbeit zum Laufen gebracht werden kann. Anzumerken sei zudem, dass bei Beobachtung der Entwicklerarbeitsplätze in Deutschland dort zunehmend mehr englische Versionen anzutreffen sind.

Für das Beispiel soll angenommen werden, dass die Anwendung auf der "grünen Wiese" beginnt. Es gibt also weder irgendwelche Logik noch Datenbankstrukturen. Dieser Fall ist ein gutes Szenario, um sich vom relationalen Datenbankdesign zu lösen und das Objektmodell in den Mittelpunkt des Designs zu stellen. Statt also "klassisch" als Erstes eine Datenbank zu gestalten und dann eine Datenzugriffsschicht dafür zu programmieren, entsteht ein Objektmodell, aus dem der Entwickler eine Datenbank generiert. Der Ansatz heißt "Model First" und wird vom ADO.NET Entity Framework (EF), dem objektrelationalen Mapper (ORM) von .NET, zur Verfügung gestellt.

Die Geschäftsobjekte

Das erste Projekt soll die Heimat für die Geschäftsobjekte sein. Nach dem Start von Visual Studio ist es unter File | New Project anzulegen. Dann wählt der Entwickler als Projekttyp Visual C# | Class Library aus und gibt als Namen "WWWings_GO" an. Als "Solution Name" für die Projektmappe wählt er "WWWings". Bei "Location" hat er einen Pfad anzugeben, der keine Leerzeichen enthalten darf (siehe Abb. 1). Wenn man den Team Foundation Server (TFS) oder eine andere, in Visual Studio integrierte Quellcodeverwaltung verwendet, kann man auch "Add to Source Control" wählen und einen Pfad in der Quellcodeverwaltung für das Projekt festlegen.


Projekt mit Projektmappe anlegen (Abb. 1)

Danach löscht der Entwickler im angelegten Projekt die Datei Class1 und fügt statt ihr mit Add New Item ein Element des Typs "ADO.NET Entity Data Model" mit Namen WWWingsModell.edmx hinzu. Im folgenden Dialog wählt er "Empty Model", da es ja keine Datenbank gibt. Nun gilt es, mit den drei Elementen der Werkzeugleiste (Entity, Association und Inheritance) das in Abbildung 2 dargestellte Modell zusammenzuklicken. Das geht recht intuitiv, hier sollen nur einige Hinweise gegeben werden: Die vier Entitäten "Person", "Passagier", "Pilot" und "Flug" legt der Entwickler mit dem Element "Entity" aus der Werkzeugleiste an. "Pilot" und "Passagier" erben von "Person", das erreicht man mit dem Element "Inheritance", das man von der erbenden auf die vererbende Klasse zieht. Die automatisch erzeugten "Id"-Elemente in den abgeleiteten Klassen muss der Entwickler händisch löschen. Die weiteren Attribute der Klasse sind vom Typ "string", außer den Attributen mit "Datum" im Namen. Diese sind im "Properties Window" auf "DateTime" zu setzen.


Das zu erzeugende Objektmodell (Abb. 2)

Bei "Geburtsdatum" sollte man außerdem "Nullable" auf "false" stellen; diese Angabe ist optional. "Plaetze" und "FreiePlaetze" müssen den Typ "Int16" zugewiesen bekommen. Bei "Flug.ID" setzt man zudem "StoreGenerationPattern" auf "None". Damit werden die Primärschlüssel für Flüge nicht automatisch vergeben, sondern man kann diese im Sinne von Nummernkreisen selbst eintragen. Die Personen-IDs soll hingegen die Datenbank vergeben (Autowerte). Daher belässt man dort den Eintrag auf "Identity".

Zwischen "Passagier" und "Flug" gibt es eine n:m-Beziehung (many to many), zwischen "Pilot" und "Flug" eine 1:n-Beziehung (one to many) – die Rationalisierung hat also auch im Cockpit zugeschlagen, indem der Co-Pilot abgeschafft wurde (wie von Ryanair vorgeschlagen). Die Beziehungen legt man besser nicht über das Element "Association" aus der Werkzeugleiste an, sondern indem man Add | Association im Kontextmenü in der Designeroberfläche wählt. Bei der 1:n-Beziehung sollte man "Add Foreign Key properties" wählen, damit man später Beziehungen zwischen "Flug" und "Pilot" auch auf Schlüsselebene herstellen kann und dafür nicht notwendigerweise den "Pilot" komplett laden muss. Die "PilotId" in Flüge muss man daher nicht eingeben; durch das Erstellen der Assoziation zu "Pilot" wird dieses Element automatisch erzeugt.


Anlegen einer 1:n-Assoziation mit zusätzlicher Fremdschlüsselbeziehung (Abb. 3)

Durch das Zusammenklicken des Objektmodells entstehen fünf .NET-Klassen in einer Datei WWWingsModell.Designer.cs, die man aber nur sieht, wenn man im Solution Explorer die Option
"Show All Files" gewählt hat: eine Klasse für jede Entität (Entitätsklassen) und eine sogenannte Kontextklasse, die den Zugang zu den Instanzen der Entitätsklassen bietet. In der deutschen Version von Visual Studio kommt es hier zum oben erwähnten Fehler. Unter der hinterlegten URL [7] ist beschrieben, wie man ihn mit manuellen Eingriffen in die von Microsoft gelieferten Codegenerierungsvorlagen beseitigen kann.

Nun sind noch einige Erweiterungen für die Entitätsklasse manuell umzusetzen. Das geht einfach, weil die generierten Entitätsklassen alle als "partial" deklariert sind. Dafür muss man im Projekt eine Klassendatei
EntitaetsklassenErweiterungen.cs anlegen mit dem Inhalt aus Listing 1 (siehe oben rechts). Dadurch erhalten "Person" (und die davon abgeleiteten Klassen) sowie "Flug" jeweils ein zusätzliches Attribut, und die Ausgabe der Objekte wird erleichtert, indem ToString() aus der allgemeinen Basisklasse System.Object überschrieben wird. Ohne das würde ToString() immer nur den Klassennamen liefern. Bei den Erweiterungen ist zu bedenken, dass man hier keine zu persistierenden Attribute schaffen kann. Diese lassen sich nur im Modell anlegen.


Datenbank erzeugen

Darauf erzeugt der Entwickler aus dem Objektmodell die relationale Datenbank. Dafür ist auf der Designeroberfläche der EDMX-Datei im Kontextmenü "Generate Database from Model" auszuwählen. Im Standard muss man nun eine SQL-Server-Instanz (z. B. .\sqlexpress für den lokalen SQL Server Express) angeben. Durch ADO.NET-Entity-Framework-Treiber anderer Hersteller kann die Funktion auch für Oracle & Co. bereitstehen. Der folgende Assistent ist selbsterklärend. Der Entwickler kann eine Datenbank wählen oder den Namen einer noch nicht existierenden Datenbank eingeben; dann wird diese angelegt.

Schließlich entsteht ein SQL-Skript mit Create-Table- und Create-Index-Befehlen. Man kann es aus Visual Studio heraus mit Execute SQL in der Symbolleiste "Transact SQL Editor" ausführen (alternativ: Tastenkombination Ctrl + Shift + e). Die erzeugte Datenbank lässt sich dann im Server Explorer betrachten. Hier sollte man das Modell manuell mit Testdaten befüllen (siehe Abb. 4).


Erzeugte Datenbank und manuelle Befüllung im Server Explorer (Abb. 4)

Die Datenzugriffsschicht

Für die Datenzugriffsschicht ergänzt man die Projektmappe um ein weiteres Klassenbibliothek-Projekt mit Namen WWWings_DZS. Es benötigt einen Verweis auf das WWWings_GO-Projekt und die .NET-Framework-Assembly System.Data.Entity, die man via Add Reference hinzufügen kann. In diesem Projekt gilt es, C#-Klassen nach folgendem Muster anzulegen:

  1. Für jede der beiden zentralen Entitätsklassen gibt es eine Manager-Klasse.
  2. Die DataManager-Klasse hält jeweils eine Instanz des EF-Kontexts.
  3. Die DataManager-Klasse implementiert IDisposable und vernichtet in Dispose() den EF-Kontext.
Die Klasse FlugDataManager benötigt die Methoden GetFlug(ID), GetFluege(Abflugort, Zielort) und ReduceFreiePlatzAnzahl(ID, Platzanzahl) sowie GetFlughaefen(). Der PassagierDataManager braucht die Methoden GetPassagier(ID), GetPassagiere(Name) und AddPassagierZuFlug() sowie SavePassagiere Set(). Letztere Methode speichert eine Liste von entweder geänderten, hinzugefügten oder gelöschten Passagieren.

Listing 2 und 3 zeigen die Implementierung der Methoden mit LINQ-to-Entities-Abfragen gegen den Entity-Framework-Kontext. Zu beachten ist, dass der Kontext nur ein Attribut für den Zugriff auf alle Personen kennt. Um daraus die Passagiere zu filtern, ist der Zusatz OfType<Passagier>() notwendig. Das Filtern erfolgt aber nicht im RAM, sondern zum Glück in der Datenbank. Die Änderungen in den Objekten sind mit SaveChanges() abzuschließen. Mit SingleOrDefault() arbeitet man beim Filtern eines Datensatzes über den Primärschlüssel und erhält ein einzelnes Objekt (oder null) zurück. ToList() liefert hingegen eine Liste von Objekten. Wenn es keine gibt, ist diese leer.

Aufmerksamkeit verdient GetFluege(), denn hier wird eine LINQ-Abfrage abhängig von den Parametern aus verschiedenen Where-Bedingungen zusammengebaut. Das geschieht aufgrund der sogenannten "verzögerten Ausführung" von LINQ-to-Entities: Erst wenn die Daten wirklich gebraucht werden (z. B. ausgelöst durch SingeOrDefault() oder ToList()), wird die Abfrage in SQL umgewandelt und zur Datenbank gesendet. Solange kann man im Programmcode die Abfrage noch beliebig modifizieren.

Spannend ist auch GetFlughaefen(), da hier mit den LINQ-Operatoren Distinct() und Union() gearbeitet wird. Die ersten beiden Distinct()-Aufrufe finden in der Datenbank statt. Union() und das dritte Distinct() sind dann aber eine LINQ-to-Object-Operation im RAM, da zu dem Zeitpunkt die beiden Teillisten ja schon aus der Datenbank gelesen sind.

ApplyChanges() übernimmt in SavePassagierSet() die Änderungen an allen übergebenen Passagieren. Nur informativ wird vor dem Speichern mit SaveChanges() ausgelesen, wie viele Änderungen es zu speichern gilt. SavePassagierSet() liefert diese Änderungsinformation als out-Parameter zurück. Der eigentliche Rückgabetyp ist wieder eine Liste von Passagieren. Diese Liste enthält dann nur noch die neu angelegten Passagiere, weil diese erst durch das Speichern die Primärschlüssel-ID von der Datenbank erhalten haben. SavePassagierSet() muss diese Objekte zurückliefern, damit der Aufrufer die IDs bekommt.

Beim Verwenden einer deutschen Version von Visual Studio muss man einige Namen im Listing anpassen, zum Beispiel erzeugt Visual Studio dort "FlugMenge" und nicht "FlugSet".

Kontextverschiebung

Außerdem soll der Entity-Framework-Kontext, der standardmäßig in der Assembly liegt, wo sich die .edmx-Datei befindet (also in WWWings_GO), in die WWWings_DZS verschoben werden. Der Grund liegt darin, dass die Vermengung von EF-Entitätsklassen und -Kontext in WWWings_GO dazu führen würde, dass die Benutzeroberfläche Zugang zum Kontext hätte (siehe Abb. 5). Der Client soll aber auf keinen Fall direkt auf die Datenbank zugreifen können.


Architekturdiagramm des bisherigen Stands des Fallbeispiels (Abb. 5)

Diese Trennung ist nicht so einfach mit der Standard-Codegenerierungsvorlage des Entity Framework umzusetzen. Daher wählt man am besten auf der Designeroberfläche der .edmx-Datei zunächst eine andere Codegenerierungsvorlage mit Add Code Generation Item und dort nun den "ADO.NET Self-Tracking Entity Generator". Daraus entstehen nicht nur trennbare Klassen, sondern auch Klassen, die auf dem Client ihren Änderungsstatus selbst verfolgen können. Das ist bei physikalischer Schichtentrennung (n-Tier-Modell) eine wichtige Funktion, damit der Client selbst entscheiden kann, welche Objekte er zum Speichern zurück zum Server senden muss. Auf die physikalische Schichtentrennung geht der zweite Teil des Tutorials ein.


Stand des Projekts nach Anwendung der neuen Codegenerierungsvorlage und Verschieben der Kontextklasse (Abb. 6)
Wählt man als Dateiname WWWingsModell.tt, entstehen tatsächlich aber mehrere Dateien: WWWingsModell.tt (und ihr untergeordnet je eine Datei pro Entitätsklasse) sowie WWWingsModell.Context.tt (und ihr untergeordnet zwei Dateien für die Kontextklasse). Nun kann man WWWingsModell.Context.tt per Drag & Drop nach WWWings_DZS kopieren und dann in WWWings_GO löschen. Darauf sollte der Stand aus Abbildung 6 erreicht sein.

Damit die verschobene WWWingsModell.Context.tt-Datei zukünftige Änderungen an der .edmx-Datei berücksichtigen kann, muss der Entwickler in Zeile 13 den Pfad zur .edmx-Datei richtig setzen. Aktuell sollte dort

string inputFile = @"WWWingsModell.edmx";

stehen. Das ist durch

string inputFile = @"..\WWWings_GO\WWWingsModell.edmx";

zu ersetzen. Der Pfad sollte relativ sein. Außerdem ist dann nach Zeile 211 (nach using System.Linq;) noch

using WWWings_GO;

einzufügen. Die Zeilennummern könnten sich durch ein Software-Update natürlich verändern. Die beiden Projekte sollten nun ohne Fehler kompilierbar sein. Listing 4 zeigt außerdem eine einfache Erweiterung der Kontextklasse, die bei jedem Speichervorgang in eine Protokolldatei die Änderungen mit Datum und dem ausführenden Benutzer beschreibt.


Die Geschäftslogik

Für die Geschäftslogik legt man nun ein weiteres Projekt (WWWings_GL) an und setzt Verweise auf die Projekte WWWings_GO und WWWings_DZS sowie die .NET-Framework-Assemblies System.Data.Entity und System.Transactions.dll. Auf der Geschäftslogikebene sollen nun die Operationen nicht mehr gemäß der Entitäten, sondern anhand des Geschäftsprozesses zusammengefasst werden. Daher gibt es dort nur noch eine einzige Klasse BuchungsManager mit den sieben Methoden GetFlug(), GetFluege(), GetFlughaefen(), GetPassagier(), GetPassagiere(), SavePassagierSet() und CreateBuchung(). Sechs dieser Methoden leiten einfach an die entsprechenden Methoden in der Datenzugriffsschicht weiter. CreateBuchung() ist insofern anders, da hier zwei Manager der Datenzugriffsschicht zu verwenden sind, und es soll sichergestellt sein, dass die Platzanzahl im Flug nur reduziert wird, wenn die Zuordnung des Passagiers zum Flug auch möglich war (und umgekehrt).

Jede SaveChanges()-Ausführung ist automatisch eine Transaktion über alle, seit dem Laden beziehungsweise letzten Speichern der Änderungen in der EF-Kontextinstanz. In diesem Fall kommen aber zwei SaveChanges() in unterschiedlichen Kontexten zum Einsatz. Daher muss man eine explizite Transaktion definieren. In .NET ist das einfach mit der Klasse TransactionScope aus der Assembly System.Transactions.dll möglich. Alle innerhalb eines TransactionScope ausgeführten Datenbankoperationen bilden automatisch eine Transaktion – selbst wenn diese in verschiedenen Datenbanken und Datenbankmanagementsystemen stattfinden. Complete() markiert das Ende der Transaktion. Wird der TransactionScope vorher durch einen Laufzeitfehler verlassen, löst er automatisch ein Zurückrollen aus. Die komplette Geschäftslogik zeigt Listing 5.

Überprüfung durch den Testclient

Nun ist es an der Zeit, die Geschäftslogik mit einem ersten einfachen Client einer ersten Überprüfung zu unterziehen. Dafür bietet sich eine Konsolenanwendung an. Man muss in der Projektmappe ein weiteres neues Projekt vom Typ "Console Application" und Namen "WWWings_TestKonsole" anlegen. Das Projekt benötigt eine Referenz auf WWWings_GL und WWWings_GO sowie System.Data.Entity. Außerdem muss man die app.config-Datei aus dem Projekt WWWings_GO nach WWWings_TestKonsole kopieren, da der EF-Designer dort die bei der Startanwendung benötigte Verbindungszeichenfolge abgelegt hat.

Listing 6 zeigt den Testcode, der einen Flug abruft, einen neuen Passagier erzeugt, dessen ID ausgibt und anschließend eine Buchung durchführt. Die Flug-ID muss man entsprechend der selbst erfassten Testdaten anpassen. Nach dem erfolgreichen Kompilieren kann man die erzeugte Anwendung entweder direkt über WWWings_TestKonsole.exe im Dateisystem oder im Debugger in Visual Studio starten. Zum Debugging ist notwendig, dass man die WWWings_TestKonsole als Startanwendung festlegt, indem man in ihrem Kontextmenü "Set as Start Project" wählt. Dann wird dieses Projekt in der Projektmappe in fetter Schrift dargestellt.


Ausgabe der Testkonsole (Abb. 7)
Abbildung 7 zeigt beispielhaft das Ausführungsergebnis, wobei die ausgegebenen IDs abhängig sind von den selbst erfassten Testdaten. Das fertige Projekt zum Herunterladen [8] enthält übrigens einen Testdatengenerator, damit der Leser seine Fingerkuppen schonen kann.

Fazit

In kurzer Zeit ist eine, in mehrere wiederverwendbare logische Schichten aufgeteilte .NET-Anwendung entstanden mit einer aus einem Objektmodell generierten Datenbank, Datenzugriffsschicht, Geschäftslogik und einem Testclient. Der zweite Teil des Tutorials erweitert das Fallbeispiel zu einer verteilten Anwendungen mit "Middle Tier Services". In den weiteren Teilen entstehen dann grafische Benutzerschnittstellen (HTML mit ASP.NET, Silverlight und WPF), die auf diesen Diensten aufsetzen. (ane)


Dr. Holger Schwichtenberg
leitet das Expertenteam von www.IT-Visions.de [9], das Beratung, Schulungen und Support im Umfeld von .NET und Windows anbietet. Er hält Vorträge auf Fachkonferenzen und ist Autor zahlreicher Fachbücher.

Literatur


Listing 1: EntitaetsklassenErweiterungen.cs

namespace WWWings_GO
{

/// <summary>
/// Erweiterungen der Klasse Person, wird auch Passagier und Pilot vererbt
/// </summary>
public partial class Person
{
public string GanzerName { get { return this.Vorname + " " + this.Name; } }

public override string ToString()
{
return "Person #" + this.ID + ": " + this.GanzerName;
}
}

/// <summary>
/// Erweiterungen der Klasse Flug, wird auch Passagier und Pilot vererbt
/// </summary>
public partial class Flug
{
public string Route { get { return this.Abflugort + " -> " + this.Zielort; } }

public override string ToString()
{
return "Flug #" + this.ID + ": " + this.Route + ": " +
this.FreiePlaetze + " von " + this.Plaetze + " frei.";
}
}

}


Listing 2: Datenzugriffsschicht FlugDataManager.cs

using System;
using System.Collections.Generic;
using System.Linq;
using WWWings_GO;

namespace WWWings_DZS
{
/// <summary>
/// Datenmanager für Flüge
/// </summary>
public class FlugDataManager : IDisposable
{

// Eine Instanz des Entity-Framework-Kontextes pro Manager-Instanz
WWWingsModellContainer modell = new WWWingsModellContainer();

/// <summary>
/// Konstruktor
/// </summary>
public FlugDataManager(bool LazyLoading = false)
{
modell.ContextOptions.LazyLoadingEnabled = LazyLoading;
}


/// <summary>
/// Objekt vernichten
/// </summary>
public void Dispose()
{
modell.Dispose();
}

/// <summary>
/// Laden eines Flugs
/// </summary>
public Flug GetFlug(int FlugID)
{
var abfrage = from flug in modell.FlugSet where flug.ID ==
FlugID select flug;
return abfrage.SingleOrDefault();
}

/// <summary>
/// Laden einer Liste von Flügen
/// </summary>
public List<Flug> GetFluege(string Abflugort, string Zielort)
{
// Grundabfrage
var abfrage = from flug in modell.FlugSet select flug;
// Abfrage ggf. erweitern
if (!String.IsNullOrEmpty(Abflugort)) abfrage
= from flug in abfrage where flug.Abflugort ==
Abflugort select flug;
if (!String.IsNullOrEmpty(Zielort)) abfrage
= from flug in abfrage where flug.Zielort ==
Zielort select flug;

return abfrage.ToList();
}


/// <summary>
/// Reduzieren der Platzanzahl
/// </summary>
public bool ReducePlatzAnzahl(int FlugID, short Platzanzahl)
{
var einzelnerFlug = GetFlug(FlugID);

if (einzelnerFlug != null)
{
if (einzelnerFlug.FreiePlaetze >= Platzanzahl &&
einzelnerFlug.FreiePlaetze - Platzanzahl
<= einzelnerFlug.Plaetze)
{
// Änderung durchführen
einzelnerFlug.FreiePlaetze -= Platzanzahl;

// Speichern
modell.SaveChanges();
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}


/// <summary>
/// Liefert eine Liste aller Abflug- und Zielflughäfen
/// als Zeichenkettenliste
/// </summary>
/// <returns></returns>
public List<string> GetFlughäfen()
{
var l1 = modell.FlugSet.Select(f => f.Abflugort).Distinct();
var l2 = modell.FlugSet.Select(f => f.Zielort).Distinct();
var l3 = l1.Union(l2).Distinct();
return l3.OrderBy(z => z).ToList();
}
}
}


Listing 3: Datenzugriffsschicht PassagierDataManager.cs

using System;
using System.Collections.Generic;
using System.Linq;
using WWWings_GO;

namespace WWWings_DZS
{

/// <summary>
/// Datenmanager für Passagiere
/// </summary>
public class PassagierDataManager : IDisposable
{

// Eine Instanz des Datenkontextes pro Manager-Instanz
WWWingsModellContainer modell = new WWWingsModellContainer();

/// <summary>
/// Konstruktor
/// </summary>
public PassagierDataManager(bool LazyLoading = false)
{
modell.ContextOptions.LazyLoadingEnabled = LazyLoading;
}

/// <summary>
/// Objekt vernichten
/// </summary>
public void Dispose()
{
modell.Dispose();
}

/// <summary>
/// Holt einen Passagier
/// </summary>
public Passagier GetPassagier(int PassagierID)
{
// .OfType<Passagier>() notwendig wegen Vererbung
var abfrage = from p in modell.PersonSet.OfType<Passagier>()
where p.ID == PassagierID select p;
return abfrage.SingleOrDefault();
}

/// <summary>
/// Holt alle Passagiere mit einem Namensbestandteil
/// </summary>
public List<Passagier> GetPassagiere(string Namensbestandteil)
{
// .OfType<Passagier>() notwendig wegen Vererbung
var abfrage = from p in modell.PersonSet.OfType<Passagier>()
where p.Name.Contains(Namensbestandteil)
|| p.Vorname.Contains(Namensbestandteil) select p;
return abfrage.ToList();
}


/// <summary>
/// Füge einen Passagier zu einem Flug hinzu
/// </summary>
public bool AddPassagierZuFlug(int PassagierID, int FlugID)
{
try
{
Flug flug = modell.FlugSet.Where(f => f.ID ==
FlugID).SingleOrDefault();
Passagier passagier = modell.PersonSet.OfType<Passagier>().Where(p => p.ID == PassagierID).SingleOrDefault();
flug.Passagier.Add(passagier);

modell.SaveChanges();
return true;
}
catch (Exception ex)
{
return false;
}
}

/// <summary>
/// Änderungen an einer Liste von Passagieren speichern
/// Die neu hinzugefügten Passagiere muss die Routine wieder
/// zurückgeben, da die IDs für die neuen Passagiere erst beim
/// Speichern von der Datenbank vergeben werden
/// </summary>
public List<Passagier> SavePassagierSet(List<Passagier>
PassagierSet, out string Statistik)
{

// Änderungen für jeden einzelnen Passagier übernehmen
foreach (Passagier p in PassagierSet)
{
modell.PersonSet.ApplyChanges(p);
}

// Statistik der Änderungen zusammenstellen
Statistik = "";
Statistik += "Geändert: " + modell.ObjectStateManager
.GetObjectStateEntries(System.Data.
EntityState.Modified).Count();
Statistik += " Neu: " + modell.ObjectStateManager
.GetObjectStateEntries(System.Data.
EntityState.Added).Count();
Statistik += " Gelöscht: " + modell.ObjectStateManager
.GetObjectStateEntries(System.Data.
EntityState.Deleted).Count();

// Neue Datensätze merken, da diese nach Speichern
// zurückgegeben werden müssen (haben dann erst ihre IDs!)
List<Passagier> NeuePassagiere = PassagierSet.Where(f
=> f.ChangeTracker.State == ObjectState.Added).ToList();

// Änderungen speichern
modell.SaveChanges();

// Statistik der Änderungen zurückgeben
return NeuePassagiere;
}


}
}


Listing 4: Kontexterweiterung.cs

(Erweiterungen der Entity-Framework-Kontextklasse für Protokollierung aller Änderungen in eine CSV-Datei im Dateisystem)

using System;
using System.IO;
using System.Collections.Generic;
using System.Data.Objects;

namespace WWWings_DZS
{
/// <summary>
/// Erweiterung der Entity Framework-Kontextklasse
/// </summary>
public partial class WWWingsModellContainer
{
public static string Protokolldatei;
static WWWingsModellContainer()
{
Protokolldatei = Path.Combine(@"c:\temp", "EFLog.csv");
}


/// <summary>
/// Überschreiben von SaveChanges: Zusätzliches Protokollieren in Datei
/// </summary>
public override int SaveChanges(System.Data.Objects.SaveOptions options)
{
List<ObjectStateEntry> neue = new List<ObjectStateEntry>();

// Alle Änderngen aus den Objekten sammeln
this.DetectChanges();

// Hole geänderte
foreach (var ose in this.ObjectStateManager
.GetObjectStateEntries(System.Data.EntityState.Modified))
{
foreach (var mprop in ose.GetModifiedProperties())
{
WriteProtokoll(ose.EntitySet.Name,
(int)ose.EntityKey.EntityKeyValues[0].Value,
"Modified", mprop, ose.OriginalValues[mprop].ToString(),
ose.CurrentValues[mprop].ToString(), "");
}
}


// Hole neue
foreach (var ose in this.ObjectStateManager
.GetObjectStateEntries(System.Data.EntityState.Added))
{
// Erst mal nur merken, denn die Autowerte sind noch nicht gesetzt!
neue.Add(ose);
}


// Hole gelöschte
foreach (var ose in this.ObjectStateManager
.GetObjectStateEntries(System.Data.EntityState.Deleted))
{
WriteProtokoll(ose.EntitySet.Name, (int)ose.EntityKey
.EntityKeyValues[0].Value, "Deleted", "", "", "", "");
}

// Nun Standardimplementierung aufrufen
int Anzahl = base.SaveChanges(options);

// Jetzt noch die neuen behandeln
foreach (var ose in neue)
{
if (ose.EntityKey != null && ose.EntityKey.EntityKeyValues != null)
WriteProtokoll(ose.EntitySet.Name,(int)ose.EntityKey
.EntityKeyValues[0].Value, "Added", "", "", "", "");
}


return Anzahl;
}

/// <summary>
/// Erzeuge Protokolleintrag
/// </summary>
public void WriteProtokoll(string Entity,
int EntityID, string Aktion, string Attribut,
string AlterWert, string NeuerWert, string Text)
{
System.IO.StreamWriter sw = new System.IO.StreamWriter
(WWWingsModellContainer.Protokolldatei, true);

sw.WriteLine(System.Environment.UserDomainName + "\\"
+ System.Environment.UserName + ";" + DateTime.Now +
";" + Entity + ";" + EntityID + ";" + Aktion + ";"
+ Attribut + ";" + AlterWert +
";" + NeuerWert + ";" + Text);
sw.Close();
}

}
}


Listing 5: Geschäftslogik

using System;
using System.Collections.Generic;
using WWWings_GO;

namespace WWWings_GL
{
/// <summary>
/// Geschäftslogik für Buchungskasten
/// </summary>
public class BuchungsManager : IDisposable
{
// Instanzen der Datenmanager
WWWings_DZS.FlugDataManager fm =
new WWWings_DZS.FlugDataManager(false);
WWWings_DZS.PassagierDataManager pm =
new WWWings_DZS.PassagierDataManager(false);


/// <summary>
/// Objekt vernichten
/// </summary>
public void Dispose()
{
fm.Dispose();
pm.Dispose();
}

/// <summary>
/// Einen Flug holen
/// </summary>
public Flug GetFlug(int FNummer)
{
return fm.GetFlug(FNummer);
}

/// <summary>
/// Einen Passagier holen
/// </summary>
public Passagier GetPassagier(int PNummer)
{
return pm.GetPassagier(PNummer);
}

/// <summary>
/// Eine Liste von Flügen holen
/// </summary>
public List<Flug> GetFluege(string Abflugort, string Zielort)
{
return fm.GetFluege(Abflugort, Zielort);
}

/// <summary>
/// Eine Liste von Passagieren holen
/// </summary>
public List<Passagier> GetPassagiere(string Name)
{
return pm.GetPassagiere(Name);
}

/// <summary>
/// Änderungen an einer Liste von Passagieren speichern
/// </summary>
public List<Passagier> SavePassagierSet(List<Passagier>
PassagierSet, out string Statistik)
{
return pm.SavePassagierSet(PassagierSet, out Statistik);
}

/// <summary>
/// Flugbuchung erstellen
/// </summary>
public string CreateBuchung(int FlugID, int PassagierID)
{
try
{
// Transaktion nur erfolgreich, wenn Platzanzahl reduziert
// und Buchung erstellt!
using (System.Transactions.TransactionScope t =
new System.Transactions.TransactionScope())
{
// hier erfolgen Änderungen in Datenbanken über zwei
// Methoden der Datenzugriffsschicht
if (!fm.ReducePlatzAnzahl(FlugID, 1)) return
"Fehler: Kein Platz auf diesem Flug vorhanden!";
if (!pm.AddPassagierZuFlug(PassagierID, FlugID)) return
"Fehler: Buchung nicht möglich!";

// Transaktion erfolgreich abschließen
t.Complete();

// Buchungscode zurückgeben
return "OK";
}
}
catch (Exception ex)
{
return "Unerwarteter Fehler: " + ex.Message;
}
}


/// <summary>
/// Liste der Flüghäfen
/// </summary>
public List<string> GetFlughaefen()
{
return fm.GetFlughäfen();
}
}
}


Listing 6: Testclient, der direkt die Geschäftslogik aufruft

using System;
using System.Collections.Generic;
using WWWings_GL;
using WWWings_GO;

namespace WWWings_TestKonsole
{
class Program
{
static void Main(string[] args)
{
Console.Title = "World aus Wide Wings - Tutorial - Testkonsole";
Console.WriteLine("Start...");

FlugBuchenDemo();

Console.WriteLine("Fertig!");
Console.ReadLine();
}


private static void FlugBuchenDemo()
{

BuchungsManager bm = new BuchungsManager();

// ----------- Flug ermitteln
int FlugID = 101;
int PassagierID = 401;
Flug f = bm.GetFlug(FlugID);


if (f == null)
{
Console.WriteLine("Flug nicht gefunden!");
}
else
{
Console.WriteLine("Flug: " + f.ToString());
}

// ----------- Neuen Passagier anlegen
Passagier pneu = new Passagier();
pneu.Vorname = "Max";
pneu.Name = "Mustermann";
pneu.PassagierStatus = "C";
pneu.Geburtsdatum = DateTime.Now.AddYears(-40);

List<Passagier> GeändertePassagiere = new List<Passagier>() { pneu };
string Statistik;
var antwort = bm.SavePassagierSet(GeändertePassagiere, out Statistik);

Console.WriteLine("Statistik von SavePassagierSet: " + Statistik);

if (antwort.Count == 0)
{
Console.WriteLine("Fehler beim Anlegen des Passagiers!");
}
else
{
// Der erste neue Passagier muss der angelegte sein,
// der nun auch die ID enthält!
pneu = antwort[0];
Console.WriteLine("Passagier: " + pneu.ToString());
}

// ----------- Buchung erzeugen
var Ergebnis = bm.CreateBuchung(FlugID, pneu.ID);
Console.WriteLine("Ergebnis der Buchung: " + Ergebnis);

// ----------- Flugdaten aktualisieren
Flug fnachher = bm.GetFlug(FlugID);
if (fnachher == null)
{
Console.WriteLine("Flug nicht gefunden!");
}
else
{
Console.WriteLine("Flug: " + fnachher.ToString());
}
bm.Dispose(); // WICHTIG!!!

} // Ende FlugBuchenDemo()
} // Ende Class
} // Ende Namespace

URL dieses Artikels:
http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-1-Datenzugriff-und-Logik-1418915.html

Links in diesem Artikel:
  [1] http://www.world-wide-wings.de
  [2] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-2-Application-Server-und-Webservices-1446415.html
  [3] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-3-Eine-Weboberflaeche-mit-ASP-NET-1520357.html
  [4] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-4-Desktop-Entwicklung-mit-WPF-und-MVVM-1615881.html
  [5] http://www.heise.de/developer/artikel/Von-der-Datenbank-bis-zur-Oberflaeche-mit-NET-Teil-5-Desktop-und-Browseranwendung-mit-Silverlight-1671358.html
  [6] ftp://ftp.heise.de/pub/ix/developer/schwichtenberg_net-tut_1.zip
  [7] https://connect.microsoft.com/VisualStudio/feedback/details/557939/t4selftrackingcodegentemplatecs-german-localized-ressources-contained-quotation-marks
  [8] ftp://ftp.heise.de/pub/ix/developer/schwichtenberg_net-tut_1.zip
  [9] http://www.it-visions.de