
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf
764 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Table 22-4. Additional ADO.NET-centric Namespaces
Namespace |
Meaning in Life |
Microsoft.SqlServer.Server |
This new .NET 2.0 namespace provides types that allow you to |
|
author stored procedures via managed languages for SQL Server |
|
2005. |
System.Data |
This namespace defines the core ADO.NET types used by all data |
|
providers. |
System.Data.Common |
This namespace contains types shared between data providers, |
|
including the .NET 2.0 data provider factory types. |
System.Data.Design |
This new .NET 2.0 namespace contains various types used to |
|
construct a design-time appearance for custom data |
|
components. |
System.Data.Sql |
This new .NET 2.0 namespace contains types that allow you to |
|
discover Microsoft SQL Server instances installed on the current |
|
local network. |
System.Data.SqlTypes |
This namespace contains native data types used by Microsoft |
|
SQL Server. Although you are always free to use the corresponding |
|
CLR data types, the SqlTypes are optimized to work with SQL |
|
Server. |
|
|
Do understand that this chapter will not examine each and every type within each and every ADO.NET namespace (that task would require a large book in and of itself). However, it is quite important for you to understand the types within the System.Data namespace.
The System.Data Types
Of all the ADO.NET namespaces, System.Data is the lowest common denominator. You simply cannot build ADO.NET applications without specifying this namespace in your data access applications. This namespace contains types that are shared among all ADO.NET data providers, regardless of the underlying data store. In addition to a number of database-centric exceptions (NoNullAllowedException,
RowNotInTableException, MissingPrimaryKeyException, and the like), System.Data contains types that represent various database primitives (tables, rows, columns, constraints, etc.), as well as the common interfaces implemented by data provider objects. Table 22-5 lists some of the core types to be aware of.
Table 22-5. Core Members of the System.Data Namespace
Type |
Meaning in Life |
Constraint |
Represents a constraint for a given DataColumn object |
DataColumn |
Represents a single column within a DataTable object |
DataRelation |
Represents a parent/child relationship between two DataTable objects |
DataRow |
Represents a single row within a DataTable object |
DataSet |
Represents an in-memory cache of data consisting of any number of |
|
interrelated DataTable objects |
DataTable |
Represents a tabular block of in-memory data |
DataTableReader |
Allows you to treat a DataTable as a fire-hose cursor (forward only, read-only |
|
data access); new in .NET 2.0 |

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
765 |
Type |
Meaning in Life |
DataView |
Represents a customized view of a DataTable for sorting, filtering, searching, |
|
editing, and navigation |
IDataAdapter |
Defines the core behavior of a data adapter object |
IDataParameter |
Defines the core behavior of a parameter object |
IDataReader |
Defines the core behavior of a data reader object |
IDbCommand |
Defines the core behavior of a command object |
IDbDataAdapter |
Extends IDataAdapter to provide additional functionality of a data adapter |
|
object |
IDbTransaction |
Defines the core behavior of a transaction object |
|
|
Later in this chapter, you will get to know the role of the DataSet and its related cohorts (DataTable, DataRelation, DataRow, etc.). However, your next task is to examine the core interfaces of System.Data at a high level, to better understand the common functionality offered by any data provider. You will learn specific details throughout this chapter, so for the time being let’s simply focus on the overall behavior of each interface type.
The Role of the IDbConnection Interface
First up is the IDbConnection type, which is implemented by a data provider’s connection object. This interface defines a set of members used to configure a connection to a specific data store, and it also allows you to obtain the data provider’s transactional object. Here is the formal definition of
IDbConnection:
public interface IDbConnection : IDisposable
{
string ConnectionString { get; set; } int ConnectionTimeout { get; } string Database { get; } ConnectionState State { get; } IDbTransaction BeginTransaction();
IDbTransaction BeginTransaction(IsolationLevel il); void ChangeDatabase(string databaseName);
void Close();
IDbCommand CreateCommand(); void Open();
}
The Role of the IDbTransaction Interface
As you can see, the overloaded BeginTransaction() method defined by IDbConnection provides access to the provider’s transaction object. Using the members defined by IDbTransaction, you are able to programmatically interact with a transactional session and the underlying data store:
public interface IDbTransaction : IDisposable
{
IDbConnection Connection { get; } IsolationLevel IsolationLevel { get; } void Commit();
void Rollback();
}

766 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
The Role of the IDbCommand Interface
Next, we have the IDbCommand interface, which will be implemented by a data provider’s command object. Like other data access object models, command objects allow programmatic manipulation of SQL statements, stored procedures, and parameterized queries. In addition, command objects provide access to the data provider’s data reader type via the overloaded ExecuteReader() method:
public interface IDbCommand : IDisposable
{
string CommandText { get; set; } int CommandTimeout { get; set; }
CommandType CommandType { get; set; } IDbConnection Connection { get; set; } IDataParameterCollection Parameters { get; } IDbTransaction Transaction { get; set; } UpdateRowSource UpdatedRowSource { get; set; } void Cancel();
IDbDataParameter CreateParameter(); int ExecuteNonQuery();
IDataReader ExecuteReader();
IDataReader ExecuteReader(CommandBehavior behavior); object ExecuteScalar();
void Prepare();
}
The Role of the IDbDataParameter and IDataParameter Interfaces
Notice that the Parameters property of IDbCommand returns a strongly typed collection that implements IDataParameterCollection. This interface provides access to a set of IDbDataParameter-compliant class types (e.g., parameter objects):
public interface IDbDataParameter : IDataParameter
{
byte Precision { get; set; } byte Scale { get; set; } int Size { get; set; }
}
IDbDataParameter extends the IDataParameter interface to obtain the following additional behaviors:
public interface IDataParameter
{
DbType DbType { get; set; } ParameterDirection Direction { get; set; } bool IsNullable { get; }
string ParameterName { get; set; } string SourceColumn { get; set; }
DataRowVersion SourceVersion { get; set; } object Value { get; set; }
}
As you will see, the functionality of the IDbDataParameter and IDataParameter interfaces allows you to represent parameters within a SQL command (including stored procedures) via specific ADO.NET parameter objects rather than hard-coded string literals.

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
767 |
The Role of the IDbDataAdapter and IDataAdapter Interfaces
Data adapters are used to push and pull DataSets to and from a given data store. Given this, the IDbDataAdapter interface defines a set of properties that are used to maintain the SQL statements for the related select, insert, update, and delete operations:
public interface IDbDataAdapter : IDataAdapter
{
IDbCommand DeleteCommand { get; set; } IDbCommand InsertCommand { get; set; } IDbCommand SelectCommand { get; set; } IDbCommand UpdateCommand { get; set; }
}
In addition to these four properties, an ADO.NET data adapter also picks up the behavior defined in the base interface, IDataAdapter. This interface defines the key function of a data adapter type: the ability to transfer DataSets between the caller and underlying data store using the Fill() and Update() methods.
As well, the IDataAdapter interface allows you to map database column names to more userfriendly display names via the TableMappings property:
public interface IDataAdapter
{
MissingMappingAction MissingMappingAction { get; set; } MissingSchemaAction MissingSchemaAction { get; set; } ITableMappingCollection TableMappings { get; }
int Fill(System.Data.DataSet dataSet);
DataTable[] FillSchema(DataSet dataSet, SchemaType schemaType); IDataParameter[] GetFillParameters();
int Update(DataSet dataSet);
}
The Role of the IDataReader and IDataRecord Interfaces
The next key interface to be aware of is IDataReader, which represents the common behaviors supported by a given data reader object. When you obtain an IDataReader-compatible type from an ADO.NET data provider, you are able to iterate over the result set in a forward-only, read-only manner.
public interface IDataReader : IDisposable, IDataRecord
{
int Depth { get; } bool IsClosed { get; }
int RecordsAffected { get; } void Close();
DataTable GetSchemaTable(); bool NextResult();
bool Read();
}
Finally, as you can see, IDataReader extends IDataRecord, which defines a good number of members that allow you to extract a strongly typed value from the stream, rather than casting the generic System.Object retrieved from the data reader’s overloaded indexer method. Here is a partial listing of the various GetXXX() methods defined by IDataRecord (see the .NET Framework 2.0 SDK documentation for a complete listing):

768CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
public interface IDataRecord
{
int FieldCount { get; }
object this[ string name ] { get; } object this[ int i ] { get; }
bool GetBoolean(int i); byte GetByte(int i); char GetChar(int i);
DateTime GetDateTime(int i); Decimal GetDecimal(int i); float GetFloat(int i); short GetInt16(int i);
int GetInt32(int i); long GetInt64(int i);
...
bool IsDBNull(int i);
}
■Note The IDataReader.IsDBNull() method can be used to programmatically discover if a specified field is set to null before obtaining a value from the data reader (to avoid triggering a runtime exception).
Abstracting Data Providers Using Interfaces
At this point, you should have a better idea of the common functionality found among all .NET data providers. Recall that even though the exact names of the implementing types will differ among data providers, you are able to program against these types in a similar manner—that’s the beauty of interface-based polymorphism. Therefore, if you define a method that takes an IDbConnection parameter, you can pass in any ADO.NET connection object:
public static void OpenConnection(IDbConnection cn)
{
// Open the incoming connection for the caller. cn.Open();
}
The same holds true for a member return value. For example, consider the following simple C# program, which allows the caller to obtain a specific connection object using the value of a custom enumeration (assume you have “used” System.Data):
namespace ConnectionApp
{
enum DataProvider
{ SqlServer, OleDb, Odbc, Oracle }
class Program
{
static void Main(string[] args)
{
// Get a specific connection.
IDbConnection myCn = GetConnection(DataProvider.SqlServer);
// Assume we wish to connect to the SQL Server Pubs database. myCn.ConnectionString =
"Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs";

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
769 |
//Now open connection via our helper function.
OpenConnection(myCn);
//Use connection and close when finished.
...
myCn.Close();
}
static IDbConnection GetConnection(DataProvider dp)
{
IDbConnection conn = null; switch (dp)
{
case DataProvider.SqlServer: conn = new SqlConnection(); break;
case DataProvider.OleDb:
conn = new OleDbConnection(); break;
case DataProvider.Odbc:
conn = new OdbcConnection(); break;
case DataProvider.Oracle:
conn = new OracleConnection(); break;
}
return conn;
}
}
}
The benefit of working with the general interfaces of System.Data is that you have a much better chance of building a flexible code base that can evolve over time. For example, perhaps today you are building an application targeting Microsoft SQL Server, but what if your company switches to Oracle months down the road? If you hard-code the types of System.Data.SqlClient, you will obviously need to edit, recompile, and redeploy the assembly.
Increasing Flexibility Using Application
Configuration Files
To further increase the flexibility of your ADO.NET applications, you could incorporate a client-side *.config file that makes use of custom key/value pairs within the <appSettings> element. Recall from Chapter 11 that custom data can be programmatically obtained using types within the System.Configuration namespace. For example, assume you have specified the connection string and data provider values within a configuration file as so:
<configuration>
<appSettings>
<add key="provider" value="SqlServer" /> <add key="cnStr" value=
"Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs"/> </appSettings>
</configuration>

770 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
With this, you could update Main() to programmatically read these values. By doing so, you essentially build a data provider factory. Here are the relevant updates:
static void Main(string[] args)
{
// Read the provider key.
string dpStr = ConfigurationManager.AppSettings["provider"]; DataProvider dp = (DataProvider)Enum.Parse(typeof(DataProvider), dpStr);
// Read the cnStr.
string cnStr = ConfigurationManager.AppSettings["cnStr"];
// Get a specific connection.
IDbConnection myCn = GetConnection(dp); myCn.ConnectionString = cnStr;
...
}
■Note The ConfigurationManager type is new to .NET 2.0. Be sure to set a reference to the System.Configuration.dll assembly and “use” the System.Configuration namespace.
If the previous example were reworked into a .NET code library (rather than a console application), you would be able to build any number of clients that could obtain specific connections using various layers of abstraction. However, to make a worthwhile data provider factory library, you would also have to account for command objects, data readers, data adapters, and other data-centric types. While building such a code library would not necessarily be difficult, it would require a good amount of code. Thankfully, as of .NET 2.0, the kind folks in Redmond have built this very thing into the base class libraries.
■Source Code The MyConnectionFactory project is included under the Chapter 22 subdirectory.
The .NET 2.0 Provider Factory Model
Under .NET 2.0, we are now offered a data provider factory pattern that allows us to build a single code base using generalized data access types. Furthermore, using application configuration files (and the spiffy new <connectionStrings> section), we are able to obtain providers and connection strings declaratively without the need to recompile or redeploy the client software.
To understand the data provider factory implementation, recall from Table 22-1 that the objects within a data provider each derive from the same base classes defined within the
System.Data.Common namespace:
•DbCommand: Abstract base class for all command objects
•DbConnection: Abstract base class for all connection objects
•DbDataAdapter: Abstract base class for all data adapter objects
•DbDataReader: Abstract base class for all data reader objects
•DbParameter: Abstract base class for all parameter objects
•DbTransaction: Abstract base class for all transaction objects

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
771 |
In addition, as of .NET 2.0, each of the Microsoft-supplied data providers now provides a specific class deriving from System.Data.Common.DbProviderFactory. This base class defines a number of methods that retrieve provider-specific data objects. Here is a snapshot of the relevant members of DbProviderFactory:
public abstract class DbProviderFactory
{
...
public virtual DbCommand CreateCommand();
public virtual DbCommandBuilder CreateCommandBuilder(); public virtual DbConnection CreateConnection();
public virtual DbConnectionStringBuilder CreateConnectionStringBuilder(); public virtual DbDataAdapter CreateDataAdapter();
public virtual DbDataSourceEnumerator CreateDataSourceEnumerator(); public virtual DbParameter CreateParameter();
}
To obtain the DbProviderFactory-derived type for your data provider, the System.Data.Common namespace provides a class type named DbProviderFactories (note the plural in this type’s name). Using the static GetFactory() method, you are able to obtain the specific (which is to say, singular) DbProviderFactory of the specified data provider, for example:
static void Main(string[] args)
{
// Get the factory for the SQL data provider.
DbProviderFactory sqlFactory = DbProviderFactories.GetFactory("System.Data.SqlClient");
...
// Get the factory for the Oracle data provider.
DbProviderFactory oracleFactory = DbProviderFactories.GetFactory("System.Data.OracleClient");
...
}
As you might be thinking, rather than obtaining a factory using a hard-coded string literal, you could read in this information from a client-side *.config file (much like the previous MyConnectionFactory example). You will do so in just a bit. However, in any case, once you have obtained the factory for your data provider, you are able to obtain the associated provider-specific data objects (connections, commands, etc.).
Registered Data Provider Factories
Before you look at a full example of working with ADO.NET data provider factories, it is important to point out that the DbProviderFactories type (as of .NET 2.0) is able to fetch factories for only a subset of all possible data providers. The list of valid provider factories is recorded within the <DbProviderFactories> element within the machine.config file for your .NET 2.0 installation (note that the value of the invariant attribute is identical to the value passed into the DbProviderFactories. GetFactory() method):
<system.data>
<DbProviderFactories>
<add name="Odbc Data Provider" invariant="System.Data.Odbc" description=".Net Framework Data Provider for Odbc" type="System.Data.Odbc.OdbcFactory,
System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="OleDb Data Provider" invariant="System.Data.OleDb"

772 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
description=".Net Framework Data Provider for OleDb" type="System.Data.OleDb.OleDbFactory,
System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="OracleClient Data Provider" invariant="System.Data.OracleClient" description=".Net Framework Data Provider for Oracle" type="System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</DbProviderFactories>
</system.data>
If you wish to leverage a similar data provider factory pattern for DMBSs not accounted for in the machine.config file, note that the Mono distribution of .NET (see Chapter 1) provides a similar data factory that accounts for numerous open source and commercial data providers.
A Complete Data Provider Factory Example
For a complete example, let’s build a console application (named DataProviderFactory) that prints out the first and last names of individuals in the Authors table of a database named Pubs residing within Microsoft SQL Server (as you may know, Pubs is a sample database modeling a fictitious book publishing company).
First, add a reference to the System.Configuration.dll assembly and insert an app.config file to the current project and define an <appSettings> element. Remember that the format of the “official” provider value is the full namespace name for the data provider, rather than the string name of the ad hoc DataProvider enumeration used in the MyConnectionFactory example:
<configuration>
<appSettings>
<!-- Which provider? -->
<add key="provider" value="System.Data.SqlClient" />
<!-- Which connection string? -->
<add key="cnStr" value=
"Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs"/> </appSettings>
</configuration>
Now that you have a proper *.config file, you can read in the provider and cnStr values using the
ConfigurationManager.AppSettings() method. The provider value will be passed to DbProviderFactories. GetFactory() to obtain the data provider–specific factory type. The cnStr value will be used to set the ConnectionString property of the DbConnection-derived type. Assuming you have “used” the System.Data and System.Data.Common namespaces, update your Main() method as follows:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Data Provider Factories *****\n");
// Get Connection string/provider from *.config. string dp =
ConfigurationManager.AppSettings["provider"]; string cnStr =

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
773 |
ConfigurationManager.AppSettings["cnStr"];
// Make the factory provider.
DbProviderFactory df = DbProviderFactories.GetFactory(dp);
//Now make connection object.
DbConnection cn = df.CreateConnection();
Console.WriteLine("Your connection object is a: {0}", cn.GetType().FullName); cn.ConnectionString = cnStr;
cn.Open();
//Make command object.
DbCommand cmd = df.CreateCommand();
Console.WriteLine("Your command object is a: {0}", cmd.GetType().FullName); cmd.Connection = cn;
cmd.CommandText = "Select * From Authors";
// Print out data with data reader.
DbDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
Console.WriteLine("Your data reader object is a: {0}", dr.GetType().FullName);
Console.WriteLine("\n***** Authors in Pubs *****");
while (dr.Read())
Console.WriteLine("-> {0}, {1}", dr["au_lname"], dr["au_fname"]); dr.Close();
}
Notice that for diagnostic purposes, you are printing out the fully qualified name of the underlying connection, command, and data reader using reflection services. If you run this application, you will find that the Microsoft SQL Server provider has been used to read data from the Authors table of the Pubs database (see Figure 22-2).
Figure 22-2. Obtaining the SQL Server data provider via the .NET 2.0 data provider factory
Now, if you change the *.config file to specify System.Data.OleDb as the data provider (and update your connection string) as follows:
<configuration>
<appSettings>
<!-- Which provider? -->
<add key="provider" value="System.Data.OleDb" /> <!-- Which connection string? -->
<add key="cnStr" value=