Artikel

Juni 2003 | Artikel

Fallstricke

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0384)

ADO.NET Data Provider-Abhängigkeit vermeiden

Text: von Karl Stoiber
ADO.NET bietet viele neue Möglichkeiten, die das Entwicklerleben erleichtern. Am Anfang mögen Sie vielleicht noch zögern, aber nach und nach werden Sie ihr lieb gewonnenes ADO loslassen und sich in die neue ADO.NET-Welt mit ihren vielen neuen Möglichkeiten stürzen. Leider gibt es auch hier Fallstricke, die es zu beachten gilt. Dieser Artikel widmet sich der Problematik der .NET Data Provider-Abhängigkeit und wird hierzu eine Reihe von Lösungen anbieten.

Ein Entwickler, der bisher mit ADO gearbeitet hat, mag den Neuerungen in ADO.NET am Anfang vielleicht mit gemischten Gefühlen gegenüberstehen. Er wird viele neue Klassen kennenlernen wie DataSet, DataReader oder DataAdapter und er wird Klassen begegnen, die er bereits in ähnlicher Form aus der ADO-Welt kannte, wie z.B. Connection und Command. Alte, lieb gewonnene Freunde wie die ADO Recordset-Klasse jedoch wird er nicht mehr wiederfinden und sicherlich auch etwas vermissen.

Warum jedoch ist ADO.NET so viel anders als ADO? Bei genauerer Betrachtung wird die Antwort auf diese Frage sehr schnell deutlich. Die Änderungen von ADO nach ADO.NET sind eine notwendige Anpassung an neue Herausforderungen einer Internet-geprägten digitalen Umwelt. Diese ist geprägt von verteilten Applikationen mit lose gekoppelten Services, die mittels XML-Protokollen via HTTP miteinander kommunizieren. ADO.NET ist für diese neuen Herausforderungen viel besser geeignet als ADO dies war. Es ist nicht mehr auf ständige Verbindungen zur Datenquelle ausgelegt. XML ist integrierter Bestandteil und dabei sehr einfach zu benutzen. Nicht zuletzt eröffnen sich durch die Flexibilität und Mächtigkeit einer DataSet-Klasse neue Möglichkeiten und Daten von verschiedensten Datenquellen können in einfacher Weise miteinander kombiniert werden.
.NET Data Provider
Eine der Neuerungen in ADO.NET sind die so genannten .NET Data Provider. Aus der ADO-Welt kannte man bisher die so genannten OleDB Data Provider. Hierbei wurde ein Set von COM-basierten Schnittstellen für den Zugriff auf Datenquellen definiert. Für jede Datenquelle musste ein spezifischer OleDB Data Provider erstellt werden, der die COM-basierten Schnittstellen für die jeweilige Datenquelle implementierte. So entstanden im Lauf der Jahre zahlreiche OleDB Data Provider für Datenquellen wie SQL Server, Oracle, Microsoft Access, Microsoft Exchange u.v.m.

Das bewährte Konzept der einheitlichen Schnittstellen findet bei den .NET Data Providern ebenso Verwendung. Der wichtigste Unterschied zu den OleDB Data Providern ist jedoch, dass die .NET Data Provider in der Common Language Runtime (CLR) laufen und somit nicht mehr COM-basiert sind, d.h. sie benötigen keine COM Interoperation mehr..NET Data Provider können alle CLR-Features nutzen, was nicht zuletzt zu einer verbesserten Performance und Stabilität führt. Abpictureung 1 zeigt eine Schemadarstellung der ADO.NET-Architektur. Wesentlich hierbei ist die Differenzierung zwischen .NET Data Provider-abhängigen und -unabhängigen Klassen. Die wichtigsten abhängigen Klassen sind Connection, Command, DataReader und DataAdapter. Unabhängig dagegen sind lediglich die Klasse DataSet und die darin enthaltenen Klassen.
Welche praktische Auswirkung hat es nun, ob eine Klasse .NET Data Provider-abhängig ist oder nicht? Betrachten wir uns hierzu in Listing 1 die Funktion ListEmployees, in der FirstName und LastName der Employees-Tabelle der Northwind Datenbank des SQL Servers mittels der DataReader-Klasse selektiert werden. Die selektierten Daten werden auf der Konsole ausgegeben.

Listing 1
  1. Public Sub ListEmployees()
  2. Dim Conn As SqlConnection = New SqlConnection("Data Source=(local);Integrated Security=SSPI;Initial Catalog=northwind")
  3. Dim Cmd As SqlCommand = Conn.CreateCommand()
  4. Dim Rdr As SqlDataReader
  5. Cmd.CommandText = "SELECT FirstName, LastName FROM Employees"
  6. Conn.Open()
  7. Rdr = Cmd.ExecuteReader()
  8. Do While Rdr.Read()
  9. Console.WriteLine(vbTab & "{0}" & vbTab & "{1}", Rdr.GetString(0), Rdr.GetString(1))
  10. Loop
  11. Rdr.Close()
  12. Conn.Close()
  13. End Sub
Der hier verwendete .NET Data Provider für Microsoft SQL Server ist im Namespace System.Data.SqlClient beheimatet. Alle .NET Data Provider besitzen ihre eigenen Namespaces und diese sind entweder dem System.Data-Namespace oder einem herstellerspezifischen Data-Namespace (z.B. Microsoft.Data) untergeordnet. Das .NET Framework wird standardmäßig mit den in Tabelle 1 aufgeführten .NET Data Providern ausgeliefert.
.NET Data Provider Namespace
SQL Server .NET Data Provider System.Data.SqlClient
OleDB .NET Data Provider System.Data.OleDB
 
  Neben dem bereits erwähnten .NET Data Provider für den Microsoft SQL Server wird ein OleDB .NET Data Provider mit dem .NET Framework ausgeliefert. Dieser ermöglicht die Nutzung der bereits in ADO verwendeten OleDB Data Provider in .NET-Programmen. Da diese Provider naturgemäß COM-basiert und daher unmanaged sind, d.h. nicht in der CLR ablaufen, erfordert ihre Nutzung aus .NET-Programmen COM Interoperabilität. Dies bedeutet zusätzlichen Overhead und ist Minderung der Performance. Daher dürfte der OleDB .NET Data Provider primär in der ersten Zeit der Nutzung von ADO.NET Verwendung finden. Später, wenn die Anzahl der .NET Data Provider wächst, wird wohl vermehrt auf andere .NET Data Provider zurückgegriffen werden, die ohne COM Interop-Umwege direkt auf die jeweilige Datenquelle zugreifen. Bereits jetzt ist ein .NET Data Provider für ODBC erhältlich. Ein .NET Data Provider für Oracle ist derzeit als Beta 2 und sicherlich bald als Release erhältlich (siehe Tabelle 2).
.NET Data Provider Namespace
Oracle .NET Data Provider (Beta 2) System.Data.OracleClient
ODBC .NET Data Provider Microsoft.Data.Odbc
 
 
.NET Data Provider Abhängigkeit
Ein Austausch des .NET Data Providers in einer Applikation ist ein durchaus realistisches Szenario. Unter ADO war der Austausch von OleDB-Providern sehr einfach zu bewerkstelligen. Hier wurde der zu nutzende OleDB-Provider lediglich im ConnectionString angegeben und konnte durch Anpassen des selben, geändert werden. Im folgenden ConnectionString wurde z.B. der SQL Server Provider SQLOLEDB angegeben:
  1. con.ConnectionString = "Provider=SQLOLEDB.1;Data Source=localhost;" & _
  2.   "Initial Catalog=Northwind;Integrated Security=SSPI"
Hatte man bei der Implementierung berücksichtigt, den ConnectionString über eine Config-Datei konfigurierbar zu machen, so konnte man sehr einfach einen OleDB-Provider gegen einen anderen austauschen, ohne Code ändern zu müssen.

Versuchen wir nun, eine derartige Änderung im ADO.NET-Umfeld durchzuführen. Konkret soll der in Listing 1 verwendete SQL Server Provider durch den nicht im .NET Framework enthaltenen ODBC .NET Data Provider ausgetauscht werden um auch auf andere Datenbanken zugreifen zu können. Listing 2 zeigt die angepasste Methode ListEmployeesODBC.

Listing 2
  1. <font> Public Sub ListEmployeesODBC()</font>
  2. Dim Conn As OdbcConnection = New OdbcConnection("Driver={SQL Server};Server=(local);Database=Northwind;")
  3. Dim Cmd As OdbcCommand = Conn.CreateCommand()
  4. Dim Rdr As OdbcDataReader
  5. Cmd.CommandText = "SELECT FirstName, LastName FROM Employees"
  6. Conn.Open()
  7. Rdr = Cmd.ExecuteReader()
  8. Do While Rdr.Read()
  9. Console.WriteLine(vbTab & "{0}" & vbTab & "{1}", Rdr.GetString(0), Rdr.GetString(1))
  10. Loop
  11. Rdr.Close()
  12. Conn.Close()
  13. End Sub
Analysiert man die geänderten Codezeilen näher, so ist zu erkennen, dass neben dem ConnectionString ausschließlich die .NET Data Provider-abhängigen Klassen gegen ihre Entsprechungen im neuen Namespace ausgetauscht werden mussten. Genauer handelt es sich um die Klassen SqlConnection, SqlCommand und SqlDataReader, die gegen die Klassen OdbcConnection, OdbcCommand und OdbcDataReader ausgetauscht wurden.

Unser Ansinnen, lediglich den SQL Server .NET Data Provider gegen den ODBC .NET Data Provider auszutauschen, hat bereits in diesem kurzen Codebeispiel zu erheblichen Codeänderungen geführt. Damit wird eine Änderung, die in ADO einfach zu bewerkstelligen ist, nämlich das Austauschen von OleDB Data Providern, in ADO.NET zum Fallstrick, der umfangreiche Codeanpassungen nach sich zieht. Falls nun die Zugriffe auf eine Datenquelle nicht in einer oder mehreren Klassen gekapselt wurden, sondern diese im jeweiligen Programmkontext, in dem sie gerade benötigt werden neuerlich durchgeführt werden, dann sind die änderungsintensiven Codeteile über das gesamte Programm verstreut. Natürlich ist dies eine konzeptionelle Unsauberkeit, die sich jedoch unter ADO nicht zwangsläufig negativ bemerkbar machte, auch wenn ein OleDB Data Provider-Wechsel notwendig war. Unter ADO.NET ist dies jedoch nicht nur eine konzeptionelle Unsauberkeit, es kann durchaus umfangreiche Anpassungsaufwendungen erfordern.

Es ist wichtig, sich dieser Tatsache vor Beginn eines Projekts bewusst zu werden, um durch geeignete konzeptionelle Maßnahmen derartigen nicht nur unnötigen, sondern vor allem auch gegenüber dem Kunden schwer vertretbaren Aufwendungen entgegenwirken zu können.
Interface Polymorphismus - die Lösung?
Der Konzeption des Zugriffs auf Datenquellen ist bei ADO.NET daher eine erhöhte Aufmerksamkeit zu widmen. Aufwendungen im Rahmen des Designs sind unter anderem ein Garant vor unliebsamen Überraschungen während der Laufzeit eines Projekts oder auch später im produktiven Einsatz der Applikation. Welche Maßnahmen konzeptioneller Art kann man ergreifen, um dieses Problem zu vermeiden?

Zuerst sollten wir uns auf die Suche nach Gemeinsamkeiten der verschiedenen .NET Data Provider machen, die wir zur Lösung nutzen können. Besonders hilfreich wären hierbei gemeinsame Basisklassen, von denen alle .NET Data Provider-Klassen abgeleitet wurden oder einheitliche, von allen .NET Data Provider-Klassen implementierte Interfaces. Die Objektorientierung bietet uns in diesem Fall mit dem Polymorphismus ein sehr mächtiges Mittel, um Methoden mit einheitlicher Signatur, aber unterschiedlicher Implementierung unabhängig von der jeweiligen Klasse aufzurufen.

Betrachten wir hierzu einfach die im .NET Framework enthaltenen Beispiel-Templates für die Erstellung eigener .NET Data Provider-Klassen. Die Klassendefinitionen in diesen Templates werden einen Hinweis darauf geben, ob es gemeinsame Basisklassen oder Interfaces gibt. Hier die in den Templates enthaltenen Klassendefinitionen für die wichtigsten providerabhängigen Klassen:
  1. Public Class TemplateConnection
  2. Implements IDbConnection
  3. Public Class TemplateCommand
  4. Implements IDbCommand
  5. Public Class TemplateDataReader
  6. Implements IDataReader
  7. Implements IDataRecord
  8. Public Class TemplateDataAdapter
  9. Inherits DbDataAdapter
  10. Implements IDbDataAdapter
Wie unschwer anhand der Klassendefinitionen zu erkennen ist, werden von allen hier betrachteten Klassen einheitliche Interfaces implementiert. Alle Klassen implementieren entweder ein IDb- oder ein IData-Interface. Diese Interfaces ermöglichen durch die Trennung von Definition und Implementierung die Vereinheitlichung von Methoden, Properties und Events über alle .NET Data Provider-Implementierungen hinweg.

Definiert sind diese einheitlichen Interfaces sinnvollerweise im System.Data-Namespace. Das adäquate Mittel zur Lösung unseres Problems oder besser zur Loslösung von den .NET Data Providern heißt somit Interface-Polymorphismus. Versuchen wir nun, mit Hilfe der gemeinsamen Interfaces unseren Code aus Listing 2 .NET Data Provider-unabhängig zu implementieren. Listing 3 zeigt die geänderte Methode ListEmployeesWithInterfaces.

Listing 3
  1. Public Sub ListEmployeesWithInterfaces()
  2. Dim Conn As IDbConnection = New OdbcConnection("Driver={SQL Server};Server=(local);Database=Northwind;")
  3. Dim Cmd As IDbCommand = Conn.CreateCommand()
  4. Dim Rdr As IDataReader
  5. Cmd.CommandText = "SELECT FirstName, LastName FROM Employees"
  6. Conn.Open()
  7. Rdr = Cmd.ExecuteReader()
  8. Do While Rdr.Read()
  9. Console.WriteLine(vbTab & "{0}" & vbTab & "{1}", Rdr.GetString(0), Rdr.GetString(1))
  10. Loop
  11. Rdr.Close()
  12. Conn.Close()
  13. End Sub
Wie in Listing 3 zu sehen, kann ein Interface nicht direkt instantiiert werden, sondern es muss eine Klasse instantiiert werden, welche dieses Interface implementiert. Die Referenz dieser Klasse wird dem Interface zugewiesen. Die Interface-Methoden können somit über die zugewiesene Klassenreferenz aufgerufen werden. Leider erreicht man durch die Verwendung der IDb- und IData-Interfaces alleine nur einen Teilerfolg. Die erzeugten Objekte Connection, Command und DataReader sind durch die Verwendung von IDbConnection-, IDbCommand- und IDataReader-Interfaces nicht mehr .NET Data Provider-abhängig. Die Erzeugung dieser Objekte ist jedoch weiterhin .NET Data Provider-abhängig und weist nicht die Flexibilität auf, die ein Austauschen des .NET Data Providers ohne Codeänderungen ermöglichen würde. Wie kann nun jegliche Codeänderung bei einem .NET Data Provider-Wechsel vermieden werden?
Kombination mit dem Factory Pattern
Die Erzeugung der Objekte sollte auf eine Weise erfolgen, die es ermöglicht, den .NET Data Provider konfigurierbar und damit austauschbar zu machen, ohne dabei Codeanpassungen vornehmen zu müssen.

Um das zu erreichen, ist es notwendig, die Erzeugung der Objekte zu abstrahieren. Diese Abstraktion erreicht man durch eine Factory-Klasse, welche die Erzeugung der Objekte übernimmt. Die Erzeugung kann somit parametrisiert und so gesteuert werden, dass aus mehreren Klassen, welche alle ein gemeinsames Interface implementieren, genau die gewünschte erzeugt und das Interface mit der zugewiesenen Referenz zurückgeliefert wird. Listing 4 zeigt die Klasse SimpleFactory, welche exemplarisch die statischen Methoden GetConnection und GetCommand zur Erzeugung von Connection- und Command-Objekten anbietet.

Listing 4
  1. Imports System.Data.SqlClient
  2. Imports System.Data.OleDb
  3. Public Class SimpleFactory
  4. Public Shared Function GetConnection(dataProvType as DataProviderType) As IDbConnection
  5. Select Case dataProvType
  6. Case DataProviderType.SqlClient
  7. Return New SqlConnection()
  8. Case DataProviderType.OleDb
  9. Return New OleDbConnection()
  10. End Select
  11. End Function
  12. Public Shared Function GetCommand(dataProvType as DataProviderType) As IDbCommand
  13. Select Case dataProvType
  14. Case DataProviderType.SqlClient
  15. Return New SqlCommand()
  16. Case DataProviderType.OleDb
  17. Return New OleDbCommand()
  18. End Select
  19. End Function
  20. Public Enum DataProviderType
  21. SqlClient = 0
  22. OleDb = 1
  23. End Enum
  24. End Class
Der Vorteil der SimpleFactory-Klasse ist ihre einfache Implementierung und Handhabung. Da die Methoden zur Erzeugung von Connection- bzw. Command-Objekten statisch sind, ist die Verwendung der SimpleFactory-Klasse im Client-Code sehr einfach, da sie nicht extra erzeugt werden muss. In Listing 5 sehen Sie die dazugehörige Methode ListEmployeesWithFactory, welche die SimpleFactory-Klasse aus Listing 4 zur Erzeugung von Connection- und Command-Objekt nutzt.

Listing 5
  1. Public Sub ListEmployeesWithFactory()
  2. Dim Conn As IDbConnection = SimpleFactory.GetConnection(SimpleFactory.DataProviderType.Odbc)
  3. Dim Cmd As IDbCommand = SimpleFactory.GetCommand(SimpleFactory.DataProviderType.Odbc)
  4. Dim Rdr As IDataReader
  5. Conn.ConnectionString = "Driver={SQL Server};Server=(local);Database=Northwind;"
  6. Cmd.Connection = Conn
  7. Cmd.CommandText = "SELECT FirstName, LastName FROM Employees"
  8. Conn.Open()
  9. Rdr = Cmd.ExecuteReader()
  10. Do While Rdr.Read()
  11. Console.WriteLine(vbTab & "{0}" & vbTab & "{1}", Rdr.GetString(0), Rdr.GetString(1))
  12. Loop
  13. Rdr.Close()
  14. Conn.Close()
  15. End Sub
Es ist gut zu sehen, dass der Code nun tatsächlich .NET Data Provider-unabhängig ist und einfach durch Parametrisierung der jeweiligen Factory-Methoden ein Provider-Wechsel vorgenommen werden kann. Soll der Wechsel vollkommen ohne Codeänderung erfolgen, so müsste der Parameter für die Factory-Methoden z.B. aus der App.config-Datei ausgelesen werden. Hier eine App.config-Beispieldatei:
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. <appSettings>
  4. <add key="DataProvider" value="Odbc" />
  5. </appSettings>
  6. </configuration>
In dieser App.config wurde die im .NET Framework vordefinierte appSettings-Sektion für die Hinterlegung des zu verwendenden .NET Data Providers verwendet. Die Eintragung von eigenen Schlüssel/Werte-Paaren in dieser Sektion erfolgt mittels des add-Elements. Hier wurde für den Schlüssel DataProvider beispielhaft der Wert Odbc eingetragen. Das Auslesen des Werts in der Beispielapplikation sieht dann folgendermaßen aus:
  1. Dim ProviderType As String = New AppSettingsReader().GetValue("DataProvider", GetType(String))
Mittels der AppSettingsReader-Klasse kann der Wert für einen bestimmten Schlüssel, in diesem Fall DataProvider, aus der App.config-Sektion appSettings ausgelesen werden. Ein kleines Problem gibt es jedoch noch zu lösen. Der Wert für den Schlüssel liegt jetzt als String vor und muss für den Aufruf der GetConnection-Methode der SimpleFactory-Klasse in den entsprechenden Enum-Typ konvertiert werden. Dies erreicht man mittels der statischen Parse-Methode der Enum-Klasse. Der angegebene String, in diesem Fall der Wert Odbc wird in den entsprechenden Enum-Typ DataProviderType.Odbc umgewandelt:
  1. Dim Conn As IDbConnection = SimpleFactory.GetConnection(System.Enum.Parse(GetType(SimpleFactory.DataProviderType), ProviderType))
Die Erzeugung des Command-Objekts in Listing 5 hätte ebenso mittels des Connection-Objekts erfolgen können. Um jedoch auch anhand des Beispiels zu veranschaulichen, dass die Factory-Klasse grundsätzlich zur Erzeugung aller .NET Data Provider-abhängigen Klassen verwendet werden kann, wurde auch das Command-Objekt mittels der SimpleFactory-Klasse erzeugt. Darüber hinaus bietet es sich an, mehrere Varianten der GetConnection-Methode zu definieren, um so zum Beispiel gleichzeitig mit der Erzeugung des Connection-Objekts den ConnectionString zu initialisieren:
  1. Public Shared Function GetConnection(dataProvType as DataProviderType, connString as String)
Sollen spezielle Einstellungen für eine zu erzeugende Klasse vorgenommen werden, die nicht durch die IDb- bzw. IData-Interfaces unterstützt werden, so eignet sich auch hierfür die Factory-Klasse. Soll z.B. die PacketSize für eine SqlConnection auf einen festen Initialisierungswert gesetzt werden, der vom Default-Wert abweicht, so kann dies ebenso in der GetConnection-Methode vorgenommen werden (natürlich auch über den ConnectionString).
Dynamische Factory mittels Reflection
Die SimpleFactory-Klasse ermöglicht bereits die Konfiguration des zu verwendenden .NET Data Providers, ohne Codeänderungen durchführen zu müssen. Allerdings kann nur zwischen den bereits bei der Erstellung der SimpleFactory-Klasse vorhandenen .NET Data Providern gewechselt werden.

Nehmen wir jedoch an, dass die SimpleFactory-Klasse bereits fertiggestellt und in verschiedene Zielapplikationen als zentrale Komponente mit aufgenommen wurde und just zu diesem Zeitpunkt ein neuer XXFast .NET Data Provider für unsere primäre Zieldatenbank erscheint. Die Systembetreuer möchten diesen extrem schnellen Provider gerne einsetzen und fragen nun an, wie sie dies konfigurieren könnten. Leider müssen wir hier mit der SimpleFactory-Klasse passen, da die dynamische Integration neuer .NET Data Provider in der Implementierung nicht vorgesehen wurde.

Also sorgen wir für Abhilfe und implementieren die DynamicFactory-Klasse. Diese ist keine vollständige Neuimplementierung, sondern baut weitgehend auf der SimpleFactory-Klasse auf, sieht jedoch den Fall vor, dass ein .NET Data Provider konfiguriert wird, der bisher der Factory-Klasse nicht bekannt war und daher dynamisch geladen werden soll. Das Zauberwort hierfür heißt unter .NET natürlich Reflection.

Zuerst benötigen wir die Information, um welchen neuen .NET Data Provider es sich handelt, d.h. wir müssen in der Lage sein dessen Assembly zur Laufzeit laden zu können. Hinterlegen wir deshalb diese Information wiederum in der App.config-Datei:
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. <appSettings>
  4. <add key="DataProvider" value="Unknown" />
  5. <add key="UnknownAssembly" value="XXSoft.Data.XXFast" />
  6. </appSettings>
  7. </configuration>
Der neue XXFast .NET Data Provider wird bei der Installation im Global Assembly Cache (GAC) installiert. Wie in der App.config-Datei zu sehen ist, wurde ein neuer Enum-Wert Unknown für DataProviderType definiert. Die Enum-Deklaration für DataProviderType sieht nun folgendermaßen aus:
  1. Public Enum DataProviderType
  2. SqlClient = 0
  3. OleDb = 1
  4. Odbc = 2
  5. Unknown = 3
  6. End Enum
Nun müssen die Factory-Methoden so angepasst werden, dass beim Typ Unknown die entsprechende Assembly geladen und das gewünschte Objekt erzeugt wird. Das Laden der Assembly wird in der DynamicFactory-Klasse mit der statischen Methode Assembly.LoadWithPartialName durchgeführt. Diese hat den Vorteil, dass bei Angabe des Assembly-Namens eine im GAC installierte Assembly gefunden und geladen wird. Es wird jedoch nicht sichergestellt, dass eine exakte Version geladen wird. Soll dies erreicht werden, so muss mit der statischen Methode Assembly.Load gearbeitet werden. Diese erfordert neben der Angabe des Assembly-Namens weitere Informationen wie Version, PublicKeyToken und Culture:
  1. Dim ProviderAssembly As [Assembly] = [Assembly].LoadWithPartialName(New AppSettingsReader().GetValue("UnknownAssembly", GetType(String)))
Nachdem wir auf Basis der Angaben in der App.config-Datei die Assembly des neuen .NET Data Providers geladen haben, gilt es nun noch, das gewünschte Objekt zu erzeugen, in unserem Fall ein Connection-Objekt. Hierfür durchlaufen wir in einer Schleife die exportierten Typen der Assembly und erzeugen dasjenige Objekt, dessen Typ-Bezeichnung mit der Zeichenkette Connection endet. Wenn sich der Provider-Hersteller einigermaßen an die Namenskonventionen für .NET Data Provider gehalten hat, wird dieses Vorgehen bei allen Implementierungen zum Ziel führen. Das entsprechende Codefragment sieht folgendermaßen aus:
  1. Dim m As Type
  2. For Each m In ProviderAssembly.GetExportedTypes
  3. If m.ToString().EndsWith("Connection") Then
  4. Return ProviderAssembly.CreateInstance(m.ToString())
  5. End If
  6. Next
Die vollständige Implementierung der DynamicFactory-Methode GetConnection ist in Listing 6 zu sehen. Hiermit können Sie ohne großen Zusatzaufwand nicht nur Codeänderungen bei einem notwendigen oder gewünschten Providerwechsel vermeiden, sondern auch flexibel künftige .NET Data Provider-Optionen nutzen.

Listing 6
  1. Public Shared Function GetConnection(ByVal dataProvType As DataProviderType) As IDbConnection
  2. Select Case dataProvType
  3. Case DataProviderType.SqlClient
  4. Return New SqlConnection()
  5. Case DataProviderType.OleDb
  6. Return New OleDbConnection()
  7. Case DataProviderType.Odbc
  8. Return New OdbcConnection()
  9. Case DataProviderType.Unknown
  10. Dim ProviderAssembly As [Assembly] = [Assembly].LoadWithPartialName(New AppSettingsReader().GetValue("UnknownAssembly", GetType(String)))
  11. Dim m As Type
  12. For Each m In ProviderAssembly.GetExportedTypes
  13. If m.ToString().EndsWith("Connection") Then
  14. Return ProviderAssembly.CreateInstance(m.ToString())
  15. End If
  16. Next
  17. End Select
  18. End Function
Fazit
Lassen Sie sich den Spaß bei der Arbeit mit ADO.NET nicht verderben. Die vielen neuen Möglichkeiten verleiten sicherlich dazu, einfach nach Herzenslust loszulegen. Vergessen Sie dabei jedoch nicht die wichtige konzeptionelle Vorarbeit. Nutzen Sie bei der Konzeption und Implementierung ihrer eigenen ADO.NET-Datenzugriffsschicht die Möglichkeiten des Interface-Polymorphismus in Kombination mit dem Factory Pattern. Welche Implementierung der Factory-Klasse für Sie letztendlich die sinnvollste ist, hängt primär von Ihren Anforderungen ab. Nutzen Sie die hier dargestellten Ansätze als Grundlage und optimieren Sie diese entsprechend Ihrer Bedürfnisse.


Anzeige

Kommentare

zurück zum Seitenanfang