Design of the Data Access Application Block


The design of the Data Access Application Block focuses on providing a simple but consistent interface for performing the most common database operations needed by applications today. The combination of the DatabaseFactory class, a database provider class, and a database command wrapper class allows an application to perform database operations against relational databases while remaining decoupled from the databases that it is using. Furthermore, the design provides an extensible model that allows new database providers to be used by the application block if the ones provided with Enterprise Library don't meet the needs of an application.

This section defines these classes and describes how the DatabaseFactory, database providers, and database command wrappers work together to achieve the design goals set forth with this version of the Data Access Application Block. It also demonstrates how to extend the application block by creating and configuring a new database provider.

Database Providers

Centralizing generic data access functionality is a best practice. The patterns & practices publication entitled Architecture for .NET: Designing Applications and Services[2] refers to centralized components as Data Access Helper Components. In Enterprise Library these components are called database providers because they follow the Provider design pattern (see Chapter 1 for more information about this design pattern), and each concrete implementation is developed specifically for the type of database that it is intended to access.

[2] See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/distapp.asp.

Using database providers to centralize data access operations produces code that is cleaner and more manageable than repeating the same generic data access routines throughout an application. Database providers are core to the Data Access Application Block. They meet the criteria of a Data Access Helper Component outlined in the Architecture for .NET publication because they:

  • Abstract the data access API programming model from the data-related business logic, thus reducing and simplifying the code that typically resides in an application's data layer

  • Hide connection management semantics

  • Isolate data source location and authentication (through connection string management)

  • Encapsulate transaction enlistment

  • Centralize data access logic for easier maintenance, minimizing the need for data-source-specific coding skills throughout the development team and making it easier to troubleshoot data access issues

  • Isolate data access API versioning dependencies from data access logic components

The design goals of the Data Access Application Block consider performance, developer productivity, externalized configuration management, and database transparency. The Data Access Application Block aims to perform at 5 percent of ADO.NET efficiency, reducing the number of objects and classes, and ensuring that the block works identically independent of the database used.

Enterprise Library ships with three concrete database providers: SqlDatabase, OracleDatabase, and DB2Database. Figure 3.1 shows the relationship between the three classes that ship with Enterprise Library and the abstract base class named Database. You can find more database providers at the Enterprise Library's gotdotnet workspace [3] for data sources like OleDb and MySql.

[3] See gotdotnet.com/codegallery/codegallery.aspx?id=295a464a-6072-4e25-94e2-91be63527327.

Figure 3.1. Enterprise Library Database Providers


The Database Class

If you were familiar with previous versions of the Data Access Application Block, you will notice that many of the methods that belonged to the SqlHelper class have now been moved to a new abstract base class named Database. This is good because, prior to this version, a lot of code had to be replicated as new classes were developed to deal with data sources other than Microsoft SQL Server. Therefore, it made sense to refactor the redundant code into a base class and allow derived classes to override this functionality and add new functionality as needed.

Many of the Database class methods listed in Table 3.1 are virtual functions, so derived classes can override them should they have the need. Also, instead of returning an object specific to one data source (e.g., SqlDataReader, SqlCommand) like they did in the previous versions, these methods return objects that represent a generic data interface (e.g., IDataReader, IDbCommand).

Table 3.1. The Methods for the Database Class

Method

Description

ExecuteNonQuery

Overloaded methods used to execute commands that do not return any rows or values.

ExecuteReader

Overloaded methods used to return a class that supports the IDataReader interface. The DataReader object will contain the results returned for the arguments passed into the method.

ExecuteDataset

Overloaded methods for returning a DataSet for the arguments passed into it.

ExecuteScalar

Overloaded methods that return a single value. The value is always the first column of the first row returned by the command.

LoadDataSet

This method is similar to ExecuteDataset, except that instead of returning a new DataSet object, it adds data to an existing DataSet.

UpdateDataset

This method propagates changes recorded in a DataSet back to database.

GetConnection

Returns an instance of an IDbConnection implementation for the provider.

GetStoredProcCommandWrapper

Returns an instance of a class implementing DbCommandWrapper that wraps a stored procedure command for the provider.

GetSqlStringCommandWrapper

Returns an instance of a class implementing DbCommandWrapper that wraps a SQL query command for the provider.

GetdataAdapter

Exposes a class that returns the IDataAdapter interface.

ParameterToken

Returns the parameter token used to delimit parameters for a specific type of database.

ClearParameterCache

New to this version of the Data Access Application Block. This method clears the parameter cache. Since there is only one parameter cache that is shared by all instances of this class, this clears all parameters cached for all databases.


This level of abstraction is important because it lets you remain agnostic as to the underlying database with which you are working. (This chapter later explains how you can create an instance of a class that is derived from the Database class and execute a database command without specifying whether the data source is Microsoft SQL Server, Oracle, or DB2.) Listing 3.1 shows code written in such a way that it is agnostic as to the underlying data source. In this example, the DatabaseFactory class is used to

create an instance of a class that is derived from the Database class. The code accesses a database with the alias of PortalDatabase; however, there is no code that specifies the actual type of data source that is used. A generic DataReader is then used to populate a grid in the user interface.

Listing 3.1. Using the Database Class to Remain Agnostic to the Data Source

[C#] Database db = DatabaseFactory.CreateDatabase("PortalDatabase"); int divId = 1; using (IDataReader dataReader = db.ExecuteReader("GetCustomers",divId) {  customerGrid.DataSource = dataReader;  customerGrid.DataBind(); } [Visual Basic] Dim db As Database = DatabaseFactory.CreateDatabase("PortalDatabase") Dim divId as Integer = 1 Dim dataReader As IDataReader = db.ExecuteReader("GetCustomers",divId) customerGrid.DataSource = dataReader customerGrid.DataBind() dataReader.Close()

The SqlDatabase Class

Technically, in Listing 3.1 an instance of the Database class is not created. It is not possible to create an instance of the Database class because it is an abstract class and cannot be instantiated. Rather, you must create a derived class that overrides the abstract methods. The SqlDatabase class is the concrete database provider that ships with Enterprise Library for centralizing data access to a Microsoft SQL Server database.

Any class that derives from the Database class must override the Database class' abstract methods and properties; otherwise, it must also be marked as abstract and cannot be instantiated. The abstract methods and properties (not including overloads) of the Database class are GetConnection, GetStoredProcCommandWrapper, GetSqlStringCommandWrapper, GetdataAdapter, and ParameterToken.

When implemented in the SqlDatabase class, these methods return values that are specific to working with a Microsoft SQL Server database. For example, the GetConnection method will return a SqlConnection. The SqlDatabase class uses the features provided by the Microsoft SQL Server .NET Managed Provider to return values specific to working with Microsoft SQL Server. Table 3.2 lists the methods and properties provided by the SqlDatabase class.

Table 3.2. SqlDatabase's Methods and Properties

Method

SqlDatabase-Specific Instance

GetConnection

Returns a SqlConnection.

GetStoredProcCommandWrapper

Returns a SqlCommandWrapper.

GetSqlStringCommandWrapper

Returns a SqlCommandWrapper.

GetdataAdapter

Exposes the underlying SqlDataAdapter that is used for accessing data in a Microsoft SQL Server database.

ParameterToken

Gets the parameter token used to delimit parameters for a SQL Server database, which is the @ character.

ExecuteXmlReader

Returns an XMLReader (created from SqlCommand.ExecuteReader).


One of the methods listed in Table 3.2 is not defined by the base Database class. This method, ExecuteXmlReader, is used solely with a Microsoft SQL Server database. It is defined in the derived SqlDatabase class and not in the base Database class because it uses functionality that is not provided by every type of database. The SqlDatabase provider takes advantage of the SqlCommand object (through the SqlCommandWrapper, described later in this chapter), which provides an ExecuteXmlReader method. The ExecuteXmlReader method allows forward-only, stream-based access to XML data. Retrieving data as XML is beneficial if you need to process XML, minimize the performance overhead of creating a DataSet, and don't require a disconnected cache of data.

A drawback to using ExecuteXmlReader is that it reduces the ability to remain completely agnostic as to the underlying database, because an instance of the SqlDatabase class must be explicitly created (or cast to). Contrary to Listing 3.1 where a generic database provider was used,

Listing 3.2 illustrates explicitly using a SqlDatabase provider so the ExecuteXmlReader method can be called.

Listing 3.2. Calling SqlDatabase's ExecuteXmlReader Method

[C#] SqlDatabase db =      DatabaseFactory.CreateDatabase("PortalDatabase") as SqlDatabase; int divId = 1; DbCommandWrapper dbCommandWrapper = db.GetStoredProcCommandWrapper("GetCustomers",divId) StringBuilder readerData = new StringBuilder(); XmlTextReader reader = null; // XmlTextReader does not support IDisposable so it can't be // used with a using keyword try {     reader = (XmlTextReader)     db.ExecuteXmlReader(dbCommandWrapper.Command);     reader.MoveToContent();     for (     string value = reader.ReadOuterXml();               value != null && value.Length != 0;               value = reader.ReadOuterXml())     {          readerData.Append(value);      } } finally {      if (reader != null)      {           reader.Close();     }     dbCommandWrapper.Command.Connection.Close(); } [Visual Basic] Dim db As SqlDatabase = _     IIf(TypeOf DatabaseFactory.CreateDatabase("PortalDatabase")_     Is SqlDatabase, CType(DatabaseFactory.CreateDatabase _      ("PortalDatabase"), SqlDatabase), CType(Nothing, SqlDatabase)) Dim divId As Integer = 1 Dim dbCommandWrapper As DbCommandWrapper = _     db.GetStoredProcCommandWrapper("GetCustomers",divId) Dim readerData As StringBuilder = New StringBuilder() Dim reader As XmlTextReader = Nothing Try     reader = (XmlTextReader)     db.ExecuteXmlReader(dbCommandWrapper.Command)     reader.MoveToContent()     Dim value As String = reader.ReadOuterXml()     Do While Not value Is Nothing AndAlso _               value.Length  0          readerData.Append(value)          value = reader.ReadOuterXml()     Loop Finally     If Not reader Is Nothing Then          reader.Close()     End If     dbCommandWrapper.Command.Connection.Close() End Try

The OracleDatabase Class

Microsoft SQL Server is not the only Relational Database Management System (RDBMS) available to enterprises. Oracle is another popular choice for many. Ensuring that support for Oracle databases exists in a common data layer is a demand that must be met for any viable design.Enterprise Library's version of the Data Access Application Block goes a step further than its predecessors by providing the OracleDatabase class, a database provider for Oracle.

Just like SqlDatabase, OracleDatabase needs to override the abstract functions defined in the Database class. Instead of relying on the Microsoft SQL Server .NET Managed Provider, the OracleDatabase database provider relies on the Microsoft .NET Managed Provider for Oracle. It uses this managed provider's implementation to return values specific to working with an Oracle database. Table 3.3 lists the methods exposed by the OracleDatabase database provider.

It is worth noting that although they do not return values that are specific to using an Oracle database, the ExecuteDataSet, LoadDataSet, and ExecuteReader methods have been overridden in the OracleDatabase class. The OracleDatabase's private PrepareCWRefCursor method gives a clue as to why. All three of the overridden methods first call PrepareCWRefCursor, which adds a ref cursor as an additional output parameter to the stored procedure if a ref cursor hasn't already been added.

Oracle is unique in its use of ref cursors. A ref cursor is a pointer into a result set returned by a PL/SQL query. Unlike a normal cursor, a ref cursor is a reference to a cursor that can be set to point to different result sets at execution time. A ref cursor output parameter must be used to pass a result set from an Oracle stored procedure back to a calling application. By encapsulating this logic in the OracleDatabase class, a calling application can still call a stored procedure in a way that lets it remain agnostic as to the underlying database. That is, the intricacies involving ref cursors are hidden from any caller of the ExecuteDataSet, LoadDataSet, and ExecuteReader methods, which allows for support of the database transparency design goal.

Table 3.3. OracleDatabase's Methods and Properties

Method

OracleDatabase-Specific Instance

GetConnection

Returns an OracleConnection.

GetStoredProcCommandWrapper

Returns an OracleCommandWrapper.

GetSqlStringCommandWrapper

Returns an OracleCommandWrapper.

GeTDataAdapter

Exposes the underlying OracleDataAdapter that is used for accessing data in an Oracle database.

ParameterToken

Gets the parameter token used to delimit parameters for the Oracle Database, which is the ":" (colon) character.

ExecuteDataSet

Returns a DataSet for the arguments passed into it.

LoadDataSet

Fills a DataSet for the arguments passed into it.

ExecuteReader

Returns a DataReader for the arguments passed into it.


Another distinguishing attribute of the OracleDatabase database provider is its use of an internal OracleDataReaderWrapper class. This class implements the IDataReader interface and is used to wrap the OracleDataReader object that is returned from the ExecuteReader method. DataReaders provide strongly typed accessor methods that return column values as .NET Framework types. Examples of strongly typed accessors are GetInt32, GetString, and so on.

Most methods of the OracleDataReaderWrapper class merely pass through to the OracleDataReader class; however, a few exceptions are the GetBoolean, GetByte, and GetGuid methods. Oracle databases do not support a native data type for Guid or Bit. The OracleDataReaderWrapper class provides type conversion for these methods. The overrides for GetBoolean and GetByte use the System.Convert.ToBoolean() and System.Convert.ToByte() methods respectively for type conversions.

The GetGuid method has also been overridden to properly cast the data returned from the GetGuid method of the OracleDataReader into a proper Guid. By performing the data type conversions before results are returned from the OracleDatabase database provider, type conversions are not needed by a calling application, and the code to access the results from a database can remain agnostic as to the actual database that is being used.

One caveat: While the ExecuteReader methods will use the OracleDataReaderWrapper to perform type conversions, the LoadDataSet method will not. When a DataSet is filled by the Oracle DataAdapter, the native OracleDataReader is used, so the OracleDataReaderWrapper is of no use. If a Guid is needed by a calling application, you will need to write custom code to convert the returned byte[] value into a Guid.

The DB2Database Class

Another popular RDBMS used in many enterprises is the IBM DB2 database. While the Data Access Application Block ships with a database provider for DB2, it is not included in the default EnterpriseLibrary.sln solution file. This is because the DB2 code relies on the DB2 .NET Managed Provider from IBM (IBM.Data.DB2) and many developers may not have this provider installed. So as not to burden developers who do not have it installed to either install the provider or suffer through compiler errors, the Enterprise Library team decided not to include it in the default solution. Instead, a DB2 database provider is provided as a separate project, Data.DB2.csproj, and is included in an alternate solution file, Data.sln.

The DB2 database provider isn't much different from the other database providers mentioned so far. Just like the SQL and Oracle database providers, the DB2 database provider must override the abstract methods defined in the base Database class. The overridden methods return values that are specific to working with a DB2 database (see Table 3.4).

Database Command Wrappers

Database commands and parameters are handled differently across database systems. One way to handle the differences among types of databases is to include a multitude of overloaded methods in the Database class that perform operations like deriving and getting parameter values. This is what the versions of the Data Access Application Block preceding Enterprise Library did. In this version, however, Enterprise Library has introduced the concept of a database command wrapper.

A database command wrapper is a class that handles commands and parameters in a specific way depending on the type of database that it is intended to support. An abstract DbCommandWrapper base class provides an interface that is implemented by database-specific command wrappers. There is a one-to-one mapping between a database provider and a database-specific command wrapper. For example, the SQL database provider uses a SQL database command wrapper, and the Oracle database provider uses an Oracle database-specific command wrapper. Figure 3.2 depicts the database command wrappers that are included with Enterprise Library.

Table 3.4. DB2Database's Methods and Properties

Method

DB2Database-specific Instance

GetConnection

Returns a DB2Connection.

GetStoredProcCommandWrapper

Returns a DB2CommandWrapper.

GetSqlStringCommandWrapper

Returns a DB2CommandWrapper.

GetdataAdapter

Exposes the underlying DB2DataAdapter that is used for accessing data in a DB2 database.

ParameterToken

Gets the parameter token used to delimit parameters for the DB2 database, which is the @ character.


Figure 3.2. Enterprise Library Database Command Wrappers


Two of the abstract methods listed earlier in the section "Database Providers" are GetStoredProcCommandWrapper and GetSqlStringCommandWrapper. Both of these methods must return an object that derives from the abstract DbCommandWrapper base class. The GetStoredProcCommandWrapper returns a new database command wrapper. It requires a parameter for the name of the stored procedure and accepts optional parameters that represent the values for a stored procedure's parameters. The GetSqlStringCommandWrapper also returns a new database command wrapper but requires a string that represents a SQL statement. These command wrappers can then be used to set additional properties that might be required to update or retrieve data from an underlying database (such as parameter information required by a stored procedure). Table 3.5 lists the DbCommandWrapper's methods and properties.

Handling Parameters and the ParameterCache

In Listing 3.2, a DbCommandWrapper was returned from the Data Access Application Block by calling the GetStoredProcCommandWrapper method. The stored procedure, GetCustomers, was used to retrieve data from the backend data source. This particular stored procedure expects a single parameter that represents a company's division ID for which a set of customers should be retrieved. A significant point to recognize from this listing is that no attributes were set for this stored procedure parameter; only the value for the parameter was supplied. In this example, the Database class used the DeriveParameter methods in ADO.NET to dynamically discover attribute information about a stored procedure's parameters. This is known as dynamic parameter discovery.

One downside to dynamic parameter discovery is that it requires a separate roundtrip to the backend database to collect the information about the parameters' attributes. To minimize the impact of this additional roundtrip, the Data Access Application Block caches the information about the parameters that it discovers. The ParameterCache class provides the cache that stores the parameter information for each procedure call. Therefore, subsequent calls to the same stored procedure will not require a roundtrip.

Table 3.5. Methods and Properties of the DbCommandWrapper Class

Property/Method

Description

Command

Returns the underlying IDBCommand.

CommandTimeout

Gets or sets the wait time before terminating the attempt to execute a command and generating an error.

RowsAffected

Gets or sets the rows affected by this command.

AddInParameter

Adds a new instance of an IDataParameter object to the command set as Input.

AddOutParameter

Adds a new instance of an IDataParameter object to the command set as Output.

AddParameter

Adds a new instance of an IDataParameter object to the command.

GetParameterValue

Returns the value of the parameter for the given name.

SetParameterValue

Sets the value of a parameter for the given name.

DoAssignParameterValues

Assigns the values provided by a user to the command parameters discovered in positional order.

DoDiscoverParameters

Discovers the parameters for a stored procedure using a separate connection and command.

DoIsFurtherPreparationNeeded

Determines if a stored procedure is using parameter discovery.


An alternative to dynamic parameter discovery is to avoid the roundtrip altogether. You can use the DbCommandWrapper that is returned from the GetStoredProcCommandWrapper method to set the attributes for the stored procedure's parameters. Methods like AddInParameter or AddParameter (listed in Table 3.5) can be used to explicitly specify all of the attributes of any particular parameter. Calls can be made to these methods to explicitly specify information like the data type, direction, or size of a parameter.

Explicitly setting the attributes of a stored procedure's parameters is known as explicit parameter handling. The benefit to this approach is that it avoids any roundtrips to the database to dynamically discover information about a stored procedure's parameters. The downside is that it more tightly couples the code that makes that database calls to the stored procedures themselves. Thus, if you modify the parameters of a stored procedure, it is highly likely that you will need to modify the code to reflect these changes. Listing 3.3 illustrates how the code in Listing 3.2 can be rewritten to use the DbCommandWrapper for explicitly handling parameters.

Listing 3.3. Explicit Parameter Handling Example

[C#] SqlDatabase db =      DatabaseFactory.CreateDatabase("PortalDatabase") as SqlDatabase; int divId = 1; DbCommandWrapper dbCommandWrapper =      db.GetStoredProcCommandWrapper("GetCustomers") dbCommandWrapper.AddInParameter("DivisionID", DbType.Int32, divID); StringBuilder readerData = new StringBuilder(); XmlTextReader reader = null; // XmlTextReader does not support IDisposable so it can't be // used in a using keyword try {     reader = (XmlTextReader)     db.ExecuteXmlReader(dbCommandWrapper);      //No change to the rest of the example ... [Visual Basic] Dim db As SqlDatabase = _     IIf(TypeOf DatabaseFactory.CreateDatabase("PortalDatabase")_     Is SqlDatabase, CType(DatabaseFactory.CreateDatabase _     ("PortalDatabase"), SqlDatabase), CType(Nothing, SqlDatabase)) Dim divId As Integer = 1 Dim dbCommandWrapper As DbCommandWrapper = _     db.GetStoredProcCommandWrapper("GetCustomers") dbCommandWrapper.AddInParameter("DivisionID", DbType.Int32, divID) Dim readerData As StringBuilder = New StringBuilder() Dim reader As XmlTextReader = Nothing Try     reader = (XmlTextReader)     db.ExecuteXmlReader(dbCommandWrapper)     'No change to the rest of the example ...

Because there is a one-to-one relationship between a concrete database provider and a concrete database command wrapper, a database command wrapper is typically accessed via the specific database provider that uses it. Listing 3.3 demonstrates how to obtain a DbCommandWrapper from the SqlDatabase database provider. In this case, the DbCommandWrapper that was returned was a SqlDbCommandWrapper because of the one-to-one relationship between a SqlDatabase and a SqlDbCommandWrapper. Naturally, Enterprise Library ships with database command wrappers for Oracle and DB2 as well. I am not going to discuss the specifics of each of these in any detail because they all work similarly. All three database command wrappers override the abstract methods listed in Table 3.5 with functionality that is specific to their respective type of database.

The DatabaseFactory and DatabaseProviderFactory Classes

One of the primary factors that differentiate Enterprise Library's Data Access Application Block from its predecessors is the use of the factory pattern for creating database providers. Guidance from the patterns & practices team states that if you want to take on the goal of providing for "no touch" data source transparency, a good design pattern is to implement data access helpers (aka database providers) with a common interface so that a factory pattern can be leveraged.[4]

[4] Summarized from Application Architecture for .NET: Designing Applications and Services at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/distapp.asp.

Not only have database providers with a common interface for Microsoft SQL Server, Oracle, and DB2 been provided in Enterprise Library's version of the Data Access Application Block, but a database provider factory has also been introduced to help make the goal of database transparency more of a reality. It is still important to recognize that the patterns & practices team does not guarantee that database transparency can be achieved simply by taking advantage of the enhanced design. As shown in Listing 3.2, which used specific XML features provided by Microsoft SQL Server, there may still be situations when complete database transparency cannot be achieved.

In addition, the patterns & practices team believes that developer portability is a more common scenario than database portability (aka transparency). That is, they believe it is more common that a single developer will work with various different data sources over many different projects (e.g., working with Microsoft SQL Server on one project and Oracle on the next) than it is for a single application to require transparency among different data sources (e.g., having to work with both Microsoft SQL Server and Oracle in a single application). Whichever case may exist, however, the Data Access Application Block provides you with a consistent interface for performing database operations independent of the actual database that is being used.

When an application needs to perform some type of database operation, it usually does so by calling one of the methods listed in Table 3.1. As long as no methods are used that are specific to a single type of database provider (e.g., ExecuteXMLReader for the SqlDatabase database provider), then changing an application from relying on one data source to another is a matter of modifying the configuration data for the application. To allow for this, a factory class named DatabaseProviderFactory uses the features provided by the Configuration Application Block that retrieve configuration information. The DatabaseProviderFactory then determines which instance of a specific database provider should be created (via reflection).

By looking at Listings 3.2 and 3.3, you can infer that the application has been configured so that PortalDatabase signifies a Microsoft SQL Server database. This is known because a successful cast to a SqlDatabase database provider is possible from the value that is returned from the CreateDatabase method. If the application weren't configured for PortalDatabase to signify a Microsoft SQL Server database, then this cast would fail. Figure 3.3 illustrates the relationship between the DatabaseFactory, the DatabaseProviderFactory, and the Database class.

Figure 3.3. DatabaseFactory and DatabaseProviderFactory


The DatabaseProviderFactory exposes two public methods: CreateDatabase and CreateDefaultDatabase.CreateDatabase accepts a string argument that specifies which instance of a database should be created. In all the listings shown so far, the string PortalDatabase was provided to the CreateDatabase method so that the DatabaseProviderFactory would create the database that had been configured as the PortalDatabase database instance.

A database instance is a logical representation of a database, and it contains information about the type of database (e.g., Microsoft SQL Server, Oracle, or DB2) and the physical location of the database as represented by the database connection string. Because a database instance encapsulates this information and this information is read in using features provided by the Configuration Application Block, the code for an application can be written so that it is completely indifferent as to the physical location of a database. Furthermore, the physical location of a database can be changed (e.g., from a development to a test environment) and the application will be made aware of it quickly and without recompilation.

The CreateDefaultDatabase method does not accept any arguments. The CreateDefaultDatabase method creates a database provider that has been configured in the application as the default database; that is, the database provider that should be returned if no database instance is specified.

Another class, DatabaseFactory, refines the DatabaseProviderFactory class with static methods that simply pass through to an instance of the DatabaseProviderFactory class. This provides a very simple interface for developers, as it allows a database provider to be created without directly having to instantiate a factory class, and it contains a single method: CreateDatabase.

CreateDatabase contains two overloads: One overload accepts no arguments and wraps around the DatabaseProviderFactory's CreateDefaultDatabase method, and the other overload accepts a string and wraps around the DatabaseProviderFactory's CreateDatabase (string) method. Both the DatabaseProviderFactory and the DatabaseFactory classes can be used to obtain a database provider. In all the listings provided so far, the DatabaseFactory classand not the DatabaseProviderFactory classwas used.

Creating a Custom Database Provider

While Enterprise Library ships with database providers for three of the most common databases used by enterprise applications today, it is conceivable that many enterprises may need to develop applications that use a database other than one of these three. And while the Data Access Application Block may not supply a database provider for every database that exists, you can still reap the benefits of its design and features. To do so, however, you must create a custom database provider.

A few new database providers have cropped up since the release of Enterprise Library. You can find database providers for MySql and OleDb on the GotDotNet.com community site for Enterprise Library. The sole prerequisite for creating a database provider that can work with the Data Access Application Block is that a .NET managed data provider exists for it. This prerequisite is important because managed providers consist of a set of objects that are used to communicate between a data source and a DataSet. Therefore, if a managed data provider for a specific data store already exists, it is not too difficult to create the classes needed by the Data Access Application Block to work with that data store. However, even if a .NET managed data provider does not yet exist for a particular data source, all hope is not lost; it is possible to create one.

It is with this in mind that I have intentionally picked an example for which a .NET managed provider does not exist. This section creates a database provider that will store and retrieve data in an XML file. My primary reason for picking this database provider is to show that it is feasible to create a database provider for just about any data source that might exist.

A data store does not necessarily need to be a relational database. For example, it is not uncommon for data to be stored in XML files, especially to cache data. A common scenario is a Web site that stores nontransactional semi-static data in XML files. In fact, the well-known IBuySpy portal site uses an XML file (i.e., portalcfg.xml) for just this purpose. It stores data relevant to the site's "look and feel" in an XML file.

Having a database provider that can be used specifically for storing and retrieving data in an XML file allows for greater flexibility. Without such a provider, if the data that drives the look and feel of the IBuySpy portal needed to be moved to a data source other than an XML file, code would need to be written to access the new data source. However, if a database provider for XML files were used to store and retrieve the data, only configuration data would need to be changed to point the application at the alternate data source; no code should need to change in the application.

Since creating a .NET managed provider is really outside the scope of how a database provider uses such a component, I have not included the steps for creating the .NET managed data provider as part of this chapter. Rather, I have included step-by-step instructions for how to create the .NET managed data provider for XML files in Appendix B of this book. The rest of this section will concentrate on how to use this .NET managed data provider to create a database provider that can be used by the Data Access Application Block. This consists of creating two classes: a database command wrapper and a database provider.

Creating a Database Command Wrapper

The first step to creating a custom database provider is to create a database command wrapper class for the data store in question. A database command wrapper must derive from the abstract DbCommandWrapper base class and must override the abstract methods defined in this base class. Table 3.5 lists the methods and properties that must be overridden for a concrete database command wrapper.

The concrete database command wrapper in Listing 3.4 that supports the database provider for XML files is named XmlFileCommandWrapper. To implement support for parameter handling, the AddParameter and AddInParameter methods must be overridden to instantiate a class that implements the IDataParameter interface and that corresponds to the underlying data source. In this case that class is the XmlFileParameter. Also, the GetParameterValue and SetParameterValue methods must return and set values for this type of object.

Since the need to create an XmlFileParameter exists for many of the methods that must be overridden, I have added a CreateParameter method much like the ones that exist for the database command wrappers that ship with the Data Access Application Block. This method is used to create a new XmlFileParameter object. Listing 3.4 shows two of the functions in the XmlFileCommandWrapper that are used to create objects exposed from the .NET managed data provider that this class wraps around. This listing shows how to create a new XmlFileParameter, which exposes the IDataParameter interface, and how to create a new XMLFileCommand, which implements the IDbCommand interface.

Listing 3.4. Database Command Wrapper Methods that Create New Parameters and Commands

[C#] private XmlFileParameter CreateParameter(     string name, DbType type, int size, ParameterDirection direction,     bool nullable, byte precision, byte scale, string sourceColumn,     DataRowVersion sourceVersion, object value) {     XmlFileParameter param =                (XmlFileParameter)this.command.CreateParameter();      param.ParameterName = BuildParameterName(name);      param.DbType = type;      param.Direction = direction;      param.SourceColumn = sourceColumn;      param.SourceVersion = sourceVersion;      param.Value = (value == null)? DBNull.Value : value;      return param; } private XmlFileCommand CreateCommand(          string commandText, CommandType commandType) {     XmlFileCommand newCommand = new XmlFileCommand();     newCommand.CommandText = commandText;     newCommand.CommandType = commandType;      return newCommand; } [Visual Basic] Private Function CreateParameter(ByVal name As String, _                   ByVal type As DbType, ByVal size As Integer, _                   ByVal direction As ParameterDirection, _                   ByVal nullable As Boolean, ByVal precision As Byte, _                   ByVal scale As Byte, ByVal sourceColumn As String, _                   ByVal sourceVersion As DataRowVersion, _                   ByVal value As Object) As XmlFileParameter     Dim param As XmlFileParameter = CType( _                    Me.command.CreateParameter(), XmlFileParameter)     param.ParameterName = BuildParameterName(name)     param.DbType = type     param.Direction = direction     param.SourceColumn = sourceColumn     param.SourceVersion = sourceVersion     param.Value = IIf((value Is Nothing), DBNull.Value, value)     Return param  End Function Private Function CreateCommand(ByVal commandText As String, _                    ByVal commandType As CommandType) As XmlFileCommand     Dim newCommand As XmlFileCommand = New XmlFileCommand()     newCommand.CommandText = commandText     newCommand.CommandType = commandType     Return newCommand  End Function

Creating the Database Provider

After creating the database command wrapper, the next step is to create the class that represents the database provider itself. A database provider must derive from the abstract Database base class, and several methods and properties must be overridden. Table 3.1 lists the methods and properties that a concrete Database class must override.

Methods like GeTDataAdapter must return objects that allow the Data Access Application Block to correctly utilize the .NET managed provider for that specific data store. The GetStoredProcCommandWrapper and GetSqlStringCommandWrapper methods must return the specific command wrapper class that was written for this database provider; in this case, that is the XmlFileCommandWrapper. You can add other public methods, like ExecuteXmlReader for the SqlDatabase database provider, to the database provider as you like. For example, the XmlFileDatabase database provider shown in Listing 3.5 also includes a method that returns an XmlReader. The full source code for the XML file database provider and .NET managed data provider for XML files are on this book's Web site.

Listing 3.5. ExecuteXmlReader Method for the XML File Database Provider

[C#] public XmlReader ExecuteXmlReader(XmlFileCommandWrapper command) {     // OpenConnection calls the GetConnection method     // which returns a new XmlFileConnection class for this     // database provider     IDbConnection connection = OpenConnection();     PrepareCommand(command, connection);     XmlFileCommand xmlFileCommand = command.Command as XmlFileCommand;     try     {          return DoExecuteXmlReader(xmlFileCommand);     }     catch     {          connection.Close();          throw;     } } public XmlReader ExecuteXmlReader(          XmlFileCommandWrapper command, IDbTransaction transaction) {     PrepareCommand(command, transaction);     XmlFileCommand xmlFileCommand = command.Command as XmlFileCommand;     return DoExecuteXmlReader(xmlFileCommand); } private XmlReader DoExecuteXmlReader(XmlFileCommand xmlFileCommand) {     try     {          DateTime startTime = DateTime.Now;          //XmlFileCommand exposes a method that will return the data          //in the file in an XmlReader          XmlReader reader = xmlFileCommand.ExecuteXmlReader();          return reader;     }     catch     {          throw;     } } [Visual Basic] Public Function ExecuteXmlReader(     ByVal command As XmlFileCommandWrapper) As XmlReader     ' OpenConnection calls the GetConnection method     ' which returns a new XmlFileConnection class for this     ' database provider     Dim connection As IDbConnection = OpenConnection()     PrepareCommand(command, connection)     Dim xmlFileCommand As XmlFileCommand = _                  IIf(TypeOf command.Command Is XmlFileCommand, _                  CType(command.Command, XmlFileCommand), _                  CType(Nothing, XmlFileCommand))     Try          Return DoExecuteXmlReader(xmlFileCommand)     Catch          connection.Close()          Throw     End Try  End Function Public Function ExecuteXmlReader( _          ByVal command As XmlFileCommandWrapper, _          ByVal transaction As IDbTransaction) As XmlReader     PrepareCommand(command, transaction)     Dim xmlFileCommand As XmlFileCommand = _          IIf(TypeOf command.Command Is XmlFileCommand, _          CType(command.Command, XmlFileCommand), _          CType(Nothing, XmlFileCommand))     Return DoExecuteXmlReader(xmlFileCommand) End Function Private Function DoExecuteXmlReader( _     ByVal xmlFileCommand As XmlFileCommand) As XmlReader     Try          Dim startTime As DateTime = DateTime.Now          'XmlFileCommand exposes a method that will return the data          'in the file in an XmlReader          Dim reader As XmlReader = xmlFileCommand.ExecuteXmlReader()          Return reader     Catch          Throw     End Try  End Function

Once the database command wrapper and database provider classes have been created, the new database provider is almost ready to be used in an application. From a development perspective, the code that needs to be written to use the new database provider is no different than the code that needs to be written to access and update data for any other database provider. The only exception to this rule is if calls need to be made to methods that are specific to this database provider (e.g., ExecuteXmlReader).

The primary task you need to complete specifically for this new database provider is to add configuration data for it to the configuration information for the Data Access Application Block. The next section shows how to configure an application to use the Data Access Application Block and how to develop an application that adheres to the guidance prescribed by the Microsoft patterns & practices team while taking advantage of the benefits provided by the Data Access Application Block.




Fenster Effective Use of Microsoft Enterprise Library(c) Building Blocks for Creating Enterprise Applications and Services 2006
Effective Use of Microsoft Enterprise Library: Building Blocks for Creating Enterprise Applications and Services
ISBN: 0321334213
EAN: 2147483647
Year: 2004
Pages: 103
Authors: Len Fenster

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net