Objektorientiertes Programmieren mit .NET und C# Datenbanken Simon Bastian Abstract: Der Datenbankzugriff im .NET Framework wird durch die ADO.NET Objekte abgewickelt. Durch ADO wird die nötige Basisfunktionalität geboten und seit .NET 3.5 SP1 werden ebenfalls erweiterte Funktionen angeboten, wie z.B. Objekt relationiales Mapping. Im folgenden wird kurz auf die einzelnen Objekte von ADO.NET eingegangen und an einem Beispiel veranschaulicht wie ein Datenbankzugriff mit C# und ADO.NET aussehen könnte. Weiterhin wird das Konzept des Objekt relationalem Mappings kurz erklärt und die Umsetzung in ADO.NET sowie Alternativen vorgestellt. 1 ADO.NET Die ADO.NET Bibliotheken enthalten Klassen für die Verbindung und Übermittlung von Abfragen an Datenquellen, sowie die Verarbeitung von Ergebnissen. Im Normalfall wird ADO.NET zum Einsatz kommen, wenn eine .NET Anwendung mit einer Datenbank interagiert. Merkmale von ADO.NET sind: • Stabilität (.NET Plattform) • Unabhängigkeit von Datenbankerstellern • Plattformunabhängigkeit und Integration verteilter Komponenten (XML) • Skalierbarkeit und Performance (Datasets) Die Klassen von ADO.NET sind im Namespace System.Data zu finden. Datenprovider findet man in weiteren Namespaces wie z.B. System.Data.SqlClient oder System.Data.OleDB. 2 Objektmodell Das ADO.Net Objektmodell ist, wie in Abbildung 1 erkennbar, in zwei Teile unterteilbar. Einen verbunden- und einen unverbundenen Block. Der verbundene Block, in der Graphik die linke Hälfte, hat eine Verbindung zur Datenbank. Dieser Block stellt die typischen Funktionen für Datenbankzugriffe zur Verfügung. Und der unverbundene Block, in der Graphik die rechte Hälfte, bei dem keine Verbindung besteht. Der unverbundene Teil ist eine Besonderheit die man nicht in jeder Programmiersprache findet. Es wird die Möglichkeit geboten Daten aus der Datenbank zu sichern, zu bearbeiten und wieder in die Datenbank zu integrieren. Der DataAdapter fungiert als Brücke zwischen beiden Blöcken. Abbildung 1: Das ADO.Net Objektmodell 3 Connection Objekt Durch das Connection Objekt wird eine Verbindung zur Datenbank repräsentiert, es stellt die Methoden .Open() und .Close() bereit zum Herstellen einer Verbindung und zum Schließen dieser. Mittels einer Verbindungszeichenfolge ist es möglich ein solches Objekt zu erstellen. Um z.B. eine Verbindung mit der lokalen Instanz SQLEXPRESS und der Datenbank Northwind aufzubauen könnte man wie folgt vorgehen: connectionString = @"DataSource=.\SQLEXPRESS;Initial Catalog=Northwind;" +"Integrated Security=SSPI"; SqlConnection = new SqlConnection(connectionString) 3.1 Builder für Verbindungszeichenfolgen Da es nicht immer möglich ist Verbindungszeichenfolgen zur Entwurfs- oder Laufzeit zu erstellen bietet ADO.NET einen Builder für diese an. Die vorige Verbindungszeichenfolge könnte auch so erstellt werden: SqlConnectionStringBuilder conBuild = new SqlConnectionStringBuilder(); conBuild.DataSource = @".\SQLEXPRESS"; conBuild.InitialCatalog = "Northwind"; conBuild.IntegratedSecurity = true; Es wäre z.B. denkbar dass die Anwendung die Verbindungsinformationen aus einer Konfigurationsdatei bezieht und die Logindaten per Eingabe in einem Formular übermittelt bekommt. Weiter wichtige Eigenschaften des Builders sind UserID“ und Password“. ” ” Durch den Aufruf von .ToString() oder .ConnectionString() wird eine gültige Verbingungszeichenfolge zurückgegeben, die dem Connection Objekt übergeben werden kann. 3.2 Verbindungspooling ADO.NET führt intern ein Verbindungspooling durch, dass bedeutet offene Verbindungen werden nach dem Schließen noch nicht direkt geschlossen, sondern sie werden in einem Pool gespeichert. Wird nach längerer Zeit nicht mehr auf die Verbindung zugegriffen wird sie geschlossen. Sollte die Verbindung allerdings wieder benötigt werden ist sie bereits vorhanden. Dies kann bei vielen Verbindungen zu erheblichen Leistungsverbesserungen führen. 4 Command Objekt Die Klasse SqlCommand dient zum Ausführen von Abfragen im ADO.NET Objektmodell. Es gibt verschiedene Möglichkeiten ein Command Objekt zu erzeugen die bei anderen Objekten sehr ähnlich sind, deswegen werden sie der Vollständigkeit halber aufgezählt: // Methode des Connection-Objekts SqlCommand command = connection.CreateCommand(); // parameterloser Konstruktor SqlCommand command = new SqlCommand(); command.Connection = connection; // In Beiden fällen folgt die Zuweisung des eigentlichen Befehls command.CommandText = queryString; //Alternative: ein parametrisierter Konstruktor SqlCommand command = new SqlCommand(queryString, connection); 4.1 Methoden Methode Execute Reader ExecuteNonQuery ExecuteScalar Prepare Cancel BeginExecute... EndExecute... Funktionalität Ruft Ergebnisse in einem SqlDataReader ab. Erwartet keine Rückgabe (Insert, Update, Delete, Stored Procedures) Führt Abfrage aus, ruft erste Spalte der ersten Zeile ab. Bereitet eine Version der Abfrage vor. Bricht die Ausführung der Abfrage ab. Startet asynchrone Abfrage. Beendet Ausführung der asynchronen Abfrage. Durch BeginExcuteReader wird die asynchrone Ausführung eines DataReaders gestartet. Der Vorteil an der asynchronen Ausführung kann sein, dass nicht auf jeden einzelnen Befehl gewartet werden muss, sondern dass die Befehle asynchron ausgeführt werden. 4.2 Parametrisierte Abfragen ADO.NET unterstützt parametrisierte Abfragen, d.h. es ist möglich in Abfragen Variablen zu nutzen deren Belegung nicht statisch ist. Ein Anwendungsfall wäre z.B. ein Login bei dem ID und Password eingegeben und mit der Datenbank vergleichen werden sollen. Durch eine parametrisierte Abfrage kann man SQL Injections vorbeugen, da es nicht möglich ist den Befehl zu escapen. queryString = "SELECT COUNT(*) FROM Users" +"WHERE ID = @ID AND PW = @PW"; command.Parameters.AddWithValue("@ID", "admin"); command.Parameters.AddWithValue("@PW", "passwort"); Natürlich sollten im Normalfall keine hart codierten Daten hinterlegt werden, sondern beispielsweise bei einem Login die Nutzereingaben verwendet werden. 5 DataReader Der DataReader ist ein effizientes und kompaktes Objekt, das einen sequentiellen Lesezugriff auf die Daten ermöglicht. Die Klasse ähnelt sehr anderen Readern in .NET wie z.B. dem StreamReader oder dem TextReader. Das Objekt wird durch den Aufruf der Methode ExecuteReader des Command Objektes initialisiert. Man sollte unbedingt sobald der Reader nicht mehr benötigt wird die .Close() Methode aufrufen, um ungewollte Verbindungsprobleme zu vermeiden. Typischerweise verwendet man einen DataReader wenn man nur lesenden Zugriff auf Datensätze benötig, da er einfach am leichtgewichtigsten ist. Eine Anwendung des DataReaders könnte wie folgt aussehen: SqlDataReader reader = command.ExecuteReader(); while(reader.read()) { Console.WriteLine("ID: {0}", reader["CustomerID"]); } reader.Close(); 6 DataSet Eigentlich ist das DataSet Objekt eine Datenmenge, die Daten sind allerdings nicht mit der Datenbank verbunden. Im Gegensatz zum DataReader kann man beliebig auf die Daten zugreifen. Es ist möglich zu sortieren, zu suchen und zu filtern. Änderungen lassen sich zwischenspeichern, auf relationale Fehler überprüfen und gegen den Stand der Datenbank vergleichen. Zusätzlich ist es möglich die geänderten Daten in die Datenbank zu migrieren. Da in ADO.NET DataSet Objekte und XML Dokumente fast gleich sind, ist es leicht möglich zwischen beiden Datenstrukturen zu konvertieren. 6.1 Modell Das DataSet, siehe Abbildung 2, besteht aus DataTables die den eigentlichen Relationen in der Datenbank entsprechen, Relations den Beziehungen zwischen den Relationen, und XML Schema Informationen. Die DataTables lassen sich weiterhin in einzelene Einträge (DataRows) und Felder (DataColumns) unterteilen, mit ihren Einschränkungen (Constraints). Abbildung 2: Das ADO.NET Dataset 6.2 Methoden des DataSets Methode Clear Load Merge GetXml WriteXmlSchema 6.3 Funktionalität Entfernt alle DataRow Objekte. Lädt Daten von einem DataReader. Führt Daten mehrer DataSets zusammen. Gibt den Inhalt als XML aus. Schreibt das Schema als XML in eine Datei. DataTable Das DataTable Objekt stellt eine Relation mit den zu erwartenden Inhalten da. Die DataTable stellt eine Reihe von verschiedenen Methoden bereit, die gebräuchlichsten davon sind: Methode Funktionalität NewRow Gibt ein neues DataRow Objekt für die Table zurück. Reset Setzt die Table auf den ursprünglichen Status zurück. Load Lädt Daten von einem DataReader. Select Gibt ein Array mit DataRows zurück. GetErrors Gibt ein Array mit fehlerhaften DataRows zurück. ImportRow Importiert eine bestehende DataRow. CreateDataReader Erstellt einen DataTableReader mit dem Inhalt der Table. Das folgende Beispiel zeigt wie man einen bestehenden Eintrag eines DataTable Objekts verändern kann unter Verwendung der Find Methode und DataRows: DataTable table = DataTable("Customers"); DataRow row = table.Rows.Find("ALFKI") if (row == null) { Console.WriteLine("Kunde wurde nicht gefunden"); } else { row["CompanyName"] = "New Company Name"; } 6.4 DataView Es ist möglich Daten aus einem DataSet durch die .Select() Methode des DataTable Objekts aufzurufen, dies aber nicht immer besonders effizient. Weiterhin kann man die Ergebnisse des .Select() Aufrufs nicht an Windows- oder Webformulare binden. Deswegen wurde das DataView Objekt eingeführt, das beides ermöglicht. DataTable table = new DataTable("TabellenName"); DataView view; view = new DataView(table); Einige wichtige Methoden des DataView Objekts sind: DataView Methode Find FindRows Sort ToTable CopyTo 7 Funktionalität Durchsucht DataView nach einer DataRow. Durchsucht DataView nach mehreren DataRows. Legt die Sortierreihenfolge für den DataView fest, bzw. zeigt sie an. Erzeugt eine DataTable aus dem DataView. Kopiert DataView in ein Array. DataAdapter Die Klasse DataAdapter dient als Brücke zwischen den Daten in der Datenbank und einem DataSet, das offline verfügbar ist. Der DataAdapter kann in einem DataSet zwischengespeicherte Änderungen an die Datenbank übermitteln. Im Gegensatz zu einem Command Objekt wird das Öffnen und Schließen der Verbindung durch den DataAdapter verwaltet. DataSet ds = new DataSet(); SqlDataAdapter da = new SqlDataAdapter(queryString, connectionString); da.Fill(ds); Fill führt die Abfrage aus und speichert die Ergebnisse in einem DataSet. Durch die Methode .Update() können Änderungen an die Datenbank übermittelt werden. 8 Transaction Bietet die Möglichkeit alle Änderungen an der Datenbank auf einmal zu übermitteln. Entweder sind alle Änderungen erfolgreich oder alle werden verworfen. Ein Anwendungsszenario wäre z.B. eine Überweisung bei der das Geld erst dann gutgeschrieben wird, wenn das Abbuchen auch erfolgreich war. SqlTransaction ta = connection.BeginTransaction(); SqlCommand command = connection.CreateCommand(); command.Transaction = ta; try { command.CommandText = "INSERT INTO Customers.NewCol1 Values(’1’)"; command.ExecuteNonQuery(); command.CommandText = "INSERT INTO Customers.NewCol1 Values(’2’)"; command.ExecuteNonQuery(); ta.Commit(); } catch (Exception e) { ta.Rollback(); } 9 Beispiel Das Query gegen die Beispieldatenbank Northwind ermittelt die Kunden, deren Bestellungen von Ms. Dodsworth (EmployeeID 9) bearbeitet wurden. Der Einfachheit halber wird in dem Beispiel auf Parameter verzichtet und die Anfrage wurde nicht optimiert. Ebenfalls ist die Fehlerbehandlung für die Praxis in dieser Form nicht zu empfehlen, in den Beispielen wurde bewusst auf eine gezielte Behandlung der möglichen Fehler verzichtet. string connectionString = conBuild.ToString(); string queryString = "SELECT DISTINCT c.CustomerID, c.CompanyName" +" FROM dbo.Customers c" +" JOIN dbo.Orders o ON c.CustomerID = o.CustomerID" +" JOIN dbo.Employees e ON o.EmployeeID = e.EmployeeID" +" WHERE e.EmployeeID = 9" +" ORDER BY c.CustomerID"; using (SqlConnection connection = new SqlConnection(connectionString)) { SqlCommand command = connection.CreateCommand(); command.CommandText = queryString; try { connection.Open(); SqlDataReader reader = command.ExecuteReader(); while (reader.Read()) { Console.WriteLine("\t{0}\t{1}", reader[0], reader[1]); } reader.Close(); } catch (Exception e) { Console.WriteLine(e.Message); } } In der Konsole würden nun KundenID und Name der jeweiligen passenden Einträge ausgegeben werden. Durch den DataReader werden Strings sequentiell gelesen und ausgegeben. Es handelt sich in diesem Beispiel um keine Objekte, die zurückgegeben werden. 10 O/R Mapping Bei steigender Komplexität der Logik wird es immer schwieriger eine Übersicht über die Struktur zu bewahren. Bisher im Bezug auf ADO.NET war es nötig das Mapping zwischen Logik und Daten selbst vorzunehmen. Ein Mapper stellt eine Vermittlung zwischen Daten und Objekten da. Ein relationales Modell wird in ein Objektmodell überführt. In einem einfachen Fall werden Tabellen auf Klassen abgebildet. Jeder Datensatz enspricht der Instanz eines Objekts. Dabei müssen auch einige Punkte an das Objektmodell erfüllt werden, die von einem relationalen Datenmodell normalerweise nicht vollständig unterstüzt werden: • Objektidentität (Primärschlüssel kann auf unterschiedliche Art und Weise abgebildet werden) • Vererbung • Nebenläufigkeit • Kapselung Seit ADO.NET 3.5 SP1 ist das Entity Framework, ein O/R Mapper Bestandteil von ADO.NET. 11 11.1 ADO.NET Entity Framework Allgemein Das ADO.NET Entity Framework ist ein Objekt relationaler Mapper der seit Version 3.5 SP1 Bestandteil von .NET ist. Durch einen in Visual Studio integrierten Assistenten wird eine sehr leichte Bedienung ermöglicht, die für einfache Anwendungsszenarien ausreichend ist. Im Vergleich zu LINQ werden z.B. N:M Relationen unterstütz. LINQ2SQL unterstützt zunächst nur 1:1 Relationen, für komplexere Relationen wird eine Mappingtabelle benötigt. Zusätzlich unterstützt das ADO.NET Entity Framework eine große Auswahl an Datenbankprovidern (MS SQL Server, Oracle, DB2, MYSQL, PostgreSQL). Abbildung 3: Das ADO.NET Entityframework CSDL MSL SSDL Conceptual Schema Definition Language Mapping Schema Language Store Schema Definition Language Wie in der Graphik (Abbildung 3) erkennbar greift die eigentliche Logik nur noch auf die Conceptual“ Ebene zu. ” Also die Ebene in der die Daten als Entitäten repräsentiert werden. Die eigentlichen relationalen Daten werden in der Logical Store“ Ebene repräsentiert. ” Als Verbindung zwischen den beiden Ebenen dient die Mapping“ Ebene, die Informatio” nen über das Mapping enthält. 11.2 ADO.NET EF Beispiel Das vorige Beispiel wurde nun mit dem ADO.NET Entity Framework umgesetzt. Allerdings wurde hierbei nicht nur der Assistent genutzt, sondern auch einige Anpassungen vorgenommen. Auffallend ist hier das nun ein deutlich einfacheres Query das gewünschte Resultat liefert. using (NorthwindEntities nw = new NorthwindEntities()) { try { string queryString = "SELECT VALUE c FROM NorthwindEntities.Customers" +" AS c WHERE c.EmployeeID = 9"; ObjectQuery<Customers> custQuery = new ObjectQuery<Customers>(queryString, nw, MergeOption.NoTracking); foreach (Customers result in custQuery) { Console.WriteLine("\t{0}\t{1}",result.CustomerID, result.CompanyName); } } catch (Exception e) { Console.WriteLine(e.Message); } } Man sollte sich aber auf jedenfall vor Augen führen, dass obwohl diese Anfrage erheblich einfacher ist, der Aufwand diese Anfrage zu ermöglichen um einiges höher ist als das vergleichbare Aufwand für eine reine ADO.NET Abfrage. 12 Alternative O/R Mapper Neben dem ADO.NET Entity Framework wurde mit .NET 3.5 auch LINQ eingeführt. Allerdings gab es bereits vor dieser Entwicklung Objekt relationale Mapper für die .NET Plattform. Beispielsweise die komerzielle Lösungen Genome oder das aus der Java Welt portierte NHibernate. Im folgen wird ein kurzer Überblick über LINQ und NHibernate geliefert. Dies bedeutet aber nicht, dass Genome oder andere O/R Mapper keine Bedeutung haben. Um das Thema aber nicht zu sehr in die Tiefe zu ziehen, wird auf eine detailliertere Übersicht verzichtet. 12.1 LINQ LINQ (Language Integrated Queries) ist eine Komponente von .NET die die eine Datenabfrage direkt in die Programmiersprache integriert. Ein wesentlicher Vorteil von LINQ ist, dass der Code bereits durch den Compiler auf Fehler überprüft wird. Eine Vielzahl von Datenquellen wird unterstützt, allerdings ist in diesem Zusammenhang hauptsächlich LINQ2SQL interessant. Auf die Kombinationen von LINQ2Entities und LINQ2Datasets sei an dieser Stelle nur verwiesen, es wird nicht im Detail darauf eingegangen. LINQ2SQL ordnet dem Datenmodell einer relationalen Datenbank ein Objektmodell zu. Es handelt sich hierbei also ebenfalls um einen O/R Mapper, der im Vergleich zu eigentständigen O/R Mappern allerdings nicht den selben Funktionsumfang bietet. Durch einen in Visual Studio integrierten Designer wird eine einfache und schnelle Erstellung von Mapping Informationen ermöglicht. Für weitere Informationen zu LINQ empfiehlt sich die Ausarbeitung zu diesem speziellen Thema. 12.2 LINQ Beispiel Das folgende Beispiel zeigt die Umsetzung des hier bereits öfter vorgestellten Beispiels mit Hilfe von LINQ: DataContext dc = new DataContext(connectionString) Table<Customer> customers = dc.GetTable<Customer>(); Table<Employee> employees = dc.GetTable<Employee>(); Table<Order> orders = dc.GetTable<Order>(); var query = ( from c in customers join o in orders on c.CustomersID equals o.CusomerID join e in employees on o.EmployeeID equals e.EmployeeID where e.Employee == 9 order by c.CustomerID select new {c.CustomerID, c.CompanyName} ).Distinct(); foreach (var row in query) { Console.WriteLine("\t{0}\t{1}", row.CustomerID, row.CompanyName); } Auf den ersten Blick fällt bereits auf, dass hier ebenfalls als Rückgabetyp Objekte verwendet werden. Bezüglich des Umfangs der Abfrage gewinnt man aber nicht im Vergleich zum ADO.NET Entity Framework. 12.3 NHibernate Bei NHibernate handelt es sich um die .NET Portierung des bekannten Open Source Objekt relationalen Mappers Hibernate aus der Java Welt. NHibernate zeichnet sich besonders durch folgende Merkmale aus: • Kostenlose Open Source Software (GNU Lesser General Public License) • Bietet mit HQL (Hibernate Query Language) eine eigene Abfragesprache • Portierung von Hibernate (Erstes Release 2001) führt zu einem ausgereiften Entwicklungsstadium • Teilweise Integration von LINQ Ein großer Vorteil von NHibernate ist ohne Frage eine breite Community und eine über die Jahre ausgereifte Software. Im Gegensatz zum ADO.NET Entity Framework ist es bestimmt leichter zu speziellen Themen und Fragestellungen eine Antwort bzw. Lösung zu finden. Allerdings wird NHibernate nur noch von der Community entwickelt, dies bedeutet dass es unter Umständen langsamer auf neue Entwicklung der .NET Plattform reagiert. 13 Vergleich ADO.NET - O/R Mapping Vergleicht man die unterschiedlichen Umsetzungen des selben Beispiels fällt sehr schnell auf, dass es erhebliche Unterschiede nicht nur in dem Umfang der Abfrage sondern auch z.B. bei dem Zugriff auf die Daten gibt. Es stellt sich natürlich die Frage welche Art der Umsetzung am meisten Sinn macht. Dies lässt sich aber nicht generell beantworten, sondern ist von einigen Faktoren abhänging wie z.B.: • Performance • Komplexität • Umfang des Projekts Der Hauptgrund für reine ADO.NET Anwendungen ist auf jedenfall Performance, da die Anfragen schneller sind und das bei sehr großen Datenmengen ein durchaus entscheidender Faktor sein kann. Weiterhin ist der Aufwand, der benötigt wird um SQL Queries zu entwickeln, normalerweise auch überschaubar und nicht allzu groß. Allerdings ist die ständige Entwicklung von monotonen Abfragen nicht besonders effizient und es ist durchaus möglich eine höhere Produktivität zu erzielen, wenn der Fokus auf die eigentliche Logik und nicht die relationalen Datenabfragen gelegt werden kann. Vergleicht man nun die unterschiedlichen Objekt relationalen Mapping Lösungen in der .NET Welt dann kann man sicher sagen dass seit .NET 4.0 und der Möglichkeit LINQ2Entities eine sehr schöne und schnelle Möglichkeit geschaffen wurde, welche die Vorteile von LINQ und dem ADO.NET Entity Framework kombiniert. Ebenfalls ist ein großer Nachteil von LINQ2SQL, der zwingenden Anforderung einen MS SQL Server zu verwenden, durch die Kombination beider Komponenten leicht lösbar. Die Entwicklung wird dank der leicht bedienbaren Assistenten des Visual Studios erleichtert und für einen Großteil der Anwendungen wird das bereits ausreichend sein und es ist nicht zwingend nötig sich in Mapping Details einzuarbeiten. Die graphischen Werkzeuge sind im Vergleich zu NHibernate auch ein großer Vorteil, da man bei der Open Source Lösung ähnliche Werkzeuge bisher vermisst. 14 Zusammenfassung Mit ADO.NET und dem ADO.NET Entity Framework bietet Microsoft ein breites Spektrum an, um den Datenbankzugriff mit .NET in einer Vielzahl von Anwendungsszenarien auf eine sehr einfache und effiziente Weise zu realisieren. • ADO.Net lasst sich mit dem Visual Studio sehr einfach konfigurieren und einsetzen • Die Ähnlichkeit zu Java oder anderen Programmiersprachen ermöglicht einen schnellen Einstieg • O/R Mapping für komplexere Anwendungsszenarien wird durch das ADO.NET Entity Framework integriert Für eine einfache Anwendung wie beispielsweise ein überschaubares Reporting empfiehlt es sich wahrscheinlich eine Lösung ohne O/R Mapping zu realisieren. Für eine komplexeres Anwendungsszenario wie eine Konsolidierungslogik bietet sich möglicherweise ein O/R Mapping Framework an. Entscheidend für die Wahl der Entwicklungsmethode sind allerdings auch weitere Aspekte wie z.B. Wartung und Wiederverwendbarkeit. Für weitere Informationen ist das Microsoft Developer Network zu empfehlen. Literatur • http://msdn.microsoft.com/de-de/library/bb979090.aspx • http://msdn.microsoft.com/de-de/library/bb979079.aspx • http://msdn.microsoft.com/de-de/library/777e5ebh.aspx • http://msdn.microsoft.com/en-us/library/aa697427.aspx • http://msdn.microsoft.com/en-us/magazine/cc163399.aspx • http://msdn.microsoft.com/de-de/library/bb399572.aspx • http://msdn.microsoft.com/de-de/library/bb399567.aspx • http://msdn.microsoft.com/library/bb425822.aspx • http://msdn.microsoft.com/de-de/library/bb399360.aspx • David Sceppa: ADO.NET 2.0 Entwicklerbuch. Microsoft Press. 2006. • Wanzke & Wanzke: ADO.NET 2.0 Grundlagen und Profiwissen. Hanser. 2006. • Martin Fowler: Patterns of enterprise application architecture. Addison-Wesley Longman. 2003. • http://nhforge.org/wikis/