
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf
774 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
"Provider=SQLOLEDB.1;Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs"/> </appSettings>
</configuration>
you will find the System.Data.OleDb types are used behind the scenes (see Figure 22-3).
Figure 22-3. Obtaining the OLE DB data provider via the .NET 2.0 data provider factory
Of course, based on your experience with ADO.NET, you may be a bit unsure exactly what the connection, command, and data reader objects are actually doing. Don’t sweat the details for the time being (quite a few pages remain in this chapter, after all!). At this point, just understand that under .NET 2.0, it is possible to build a single code base that can consume various data providers in a declarative manner.
Although this is a very powerful model, you must make sure that the code base does indeed make use only of types and methods that are common to all providers. Therefore, when authoring your code base, you will be limited to the members exposed by DbConnection, DbCommand, and the other types of the System.Data.Common namespace. Given this, you may find that this “generalized” approach will prevent you from directly accessing some of the bells and whistles of a particular DBMS (so be sure to test your code!).
The <connectionStrings> Element
As of .NET 2.0, application configuration files may define a new element named <connectionStrings>. Within this element, you are able to define any number of name/value pairs that can be programmatically read into memory using the ConfigurationManager.ConnectionStrings indexer.
The chief advantage of this approach (rather than using the <appSettings> element and the ConfigurationManager.AppSettings indexer) is that you can define multiple connection strings for a single application in a consistent manner.
To illustrate, update your current app.config file as follows (note that each connection string is documented using the name and connectionString attributes rather than the key and value attributes as found in <appSettings>):
<configuration>
<appSettings>
<!-- Which provider? -->
<add key="provider" value="System.Data.SqlClient" /> </appSettings>
<connectionStrings>
<add name ="SqlProviderPubs" connectionString =
"Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs"/>

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
775 |
<add name ="OleDbProviderPubs" connectionString =
" Provider=SQLOLEDB.1;Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs"/> </connectionStrings>
</configuration>
With this, you can now update your Main() method as so:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Data Provider Factories *****\n");
string dp = ConfigurationManager.AppSettings["provider"];
string cnStr = ConfigurationManager.ConnectionStrings["SqlProviderPubs"].ConnectionString;
...
}
At this point, you should be clear on how to interact with the .NET 2.0 data provider factory (and the new <connectionStrings> element).
■Note Now that you understand the role of ADO.NET data provider factories, the remaining examples in this chapter will make explicit use of the types within System.Data.SqlClient and hard-coded connection strings, just to keep focused on the task at hand.
■Source Code The DataProviderFactory project is included under the Chapter 22 subdirectory.
Installing the Cars Database
Now that you understand the basic properties of a .NET data provider, you can begin to dive into the specifics of coding with ADO.NET. As mentioned earlier, the examples in this chapter will make use of Microsoft SQL Server. In keeping with the automotive theme used throughout this text, I have included a sample Cars database that contains three interrelated tables named Inventory, Orders, and Customers.
■Note If you do not have a copy of Microsoft SQL Server, you can download a (free) copy of Microsoft SQL Server 2005 Express Edition (http://lab.msdn.microsoft.com/express). While this tool does not have all the bells and whistles of the full version of Microsoft SQL Server, it will allow you to host the provided Cars database. Do be aware, however, that this chapter was written with Microsoft SQL Server in mind, so be sure to consult the provided SQL Server 2005 Express Edition documentation.
To install the Cars database on your machine, begin by opening the Query Analyzer utility that ships with SQL Server. Next, connect to your machine and open the provided Cars.sql file. Before you run the script, make sure that the path listed in the SQL file points to your installation of Microsoft SQL Server. Edit the following lines (in bold) as necessary:
CREATE DATABASE [Cars] ON (NAME = N'Cars_Data', FILENAME
=N' C:\Program Files\Microsoft SQL Server\MSSQL\Data\Cars_Data.MDF' , SIZE = 2, FILEGROWTH = 10%)

776 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
LOG ON (NAME = N'Cars_Log', FILENAME
= N' C:\Program Files\Microsoft SQL Server\MSSQL\Data\Cars_Log.LDF' ,
SIZE = 1, FILEGROWTH = 10%)
GO
Now run the script. Once you do, open up SQL Server Enterprise Manager. You should see three interrelated tables (with some sample data to boot) and a single stored procedure. Figure 22-4 shows the tables that populate the Cars database.
Figure 22-4. The sample Cars database
Connecting to the Cars Database from Visual Studio 2005
Now that you have the Cars database installed, you may wish to create a data connection to the database from within Visual Studio 2005. This will allow you to view and edit the various database objects from within the IDE. To do so, open the Server Explorer window using the View menu. Next, right-click the Data Connections node and select Add Connection from the context menu. From the resulting dialog box, select Microsoft SQL Server as the data source. In the next dialog box, select your machine name (or simply localhost) from the “Server name” drop-down list and specify the correct logon information. Finally, choose the Cars database from the “Select or enter a database name” drop-down list (see Figure 22-5).


778 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Understanding the Connected Layer of ADO.NET
Recall that the connected layer of ADO.NET allows you to interact with a database using the connection, command, and data reader objects of your data provider. Although you have already made use of these objects in the previous DataProviderFactory example, let’s walk through the process once again in detail. When you wish to connect to a database and read the records using a data reader object, you need to perform the following steps:
1.Allocate, configure, and open your connection object.
2.Allocate and configure a command object, specifying the connection object as a constructor argument or via the Connection property.
3.Call ExecuteReader() on the configured command object.
4.Process each record using the Read() method of the data reader.
To get the ball rolling, create a brand-new console application named CarsDataReader. The goal is to open a connection (via the SqlConnection object) and submit a SQL query (via the SqlCommand object) to obtain all records within the Inventory table of the Cars database. At this point, you will use a SqlDataReader to print out the results using the type indexer. Here is the complete code within Main(), with analysis to follow:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Data Readers *****\n");
//Create an open a connection.
SqlConnection cn = new SqlConnection(); cn.ConnectionString =
"uid=sa;pwd=;Initial Catalog=Cars; Data Source=(local)"; cn.Open();
//Create a SQL command object.
string strSQL = "Select * From Inventory"; SqlCommand myCommand = new SqlCommand(strSQL, cn);
//Obtain a data reader a la ExecuteReader().
SqlDataReader myDataReader;
myDataReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
//Loop over the results.
while (myDataReader.Read())
{
Console.WriteLine("-> Make: {0}, PetName: {1}, Color: {2}.", myDataReader["Make"].ToString().Trim(), myDataReader["PetName"].ToString().Trim(), myDataReader["Color"].ToString().Trim());
}
//Because we specified CommandBehavior.CloseConnection, we
//don't need to explicitly call Close() on the connection. myDataReader.Close();
}
}

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
779 |
Working with Connection Objects
The first step to take when working with a data provider is to establish a session with the data source using the connection object (which, as you recall, derives from DbConnection). .NET connection types are provided with a formatted connection string, which contains a number of name/value pairs separated by semicolons. This information is used to identify the name of the machine you wish to connect to, required security settings, the name of the database on that machine, and other data provider–specific information.
As you can infer from the preceding code, the Initial Catalog name refers to the database you are attempting to establish a session with (Pubs, Northwind, Cars, etc.). The Data Source name identifies the name of the machine that maintains the database (for simplicity, I have assumed no specific password is required for local system administrators).
■Note Look up the ConnectionString property of your data provider’s connection object in the .NET Framework 2.0 SDK documentation to learn about each name/value pair for your specific DBMS.
Once your construction string has been established, a call to Open() establishes your connection with the DBMS. In addition to the ConnectionString, Open(), and Close() members, a connection object provides a number of members that let you configure attritional settings regarding your connection, such as timeout settings and transactional information. Table 22-6 lists some (but not all) members of the DbConnection base class.
Table 22-6. Members of the DbConnection Type
Member |
Meaning in Life |
BeginTransaction() |
This method is used to begin a database transaction. |
ChangeDatabase() |
This method changes the database on an open connection. |
ConnectionTimeout |
This read-only property returns the amount of time to wait while |
|
establishing a connection before terminating and generating an error |
|
(the default value is 15 seconds). If you wish to change the default, |
|
specify a “Connect Timeout” segment in the connection string (e.g., |
|
Connect Timeout=30). |
Database |
This property gets the name of the database maintained by the |
|
connection object. |
DataSource |
This property gets the location of the database maintained by the |
|
connection object. |
GetSchema() |
This method returns a DataSet that contains schema information from |
|
the data source. |
State |
This property sets the current state of the connection, represented by |
|
the ConnectionState enumeration. |
|
|
As you can see, the properties of the DbConnection type are typically read-only in nature and are only useful when you wish to obtain the characteristics of a connection at runtime. When you wish to override default settings, you must alter the construction string itself. For example, the connection string sets the connection timeout setting from 15 seconds to 30 seconds (via the Connect Timeout segment of the connection string):
static void Main(string[] args)
{
SqlConnection cn = new SqlConnection();

780 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
cn.ConnectionString = "uid=sa;pwd=;Initial Catalog=Cars;" +
"Data Source=(local);Connect Timeout=30"; cn.Open();
// New helper function (see below).
ShowConnectionStatus(cn);
...
}
In the preceding code, notice you have now passed your connection object as a parameter to a new static helper method in the Program class named ShowConnectionStatus(), implemented as so:
static void ShowConnectionStatus(DbConnection cn)
{
// Show various stats about current connection object.
Console.WriteLine("***** Info about your connection *****");
Console.WriteLine("Database location: {0}", cn.DataSource); Console.WriteLine("Database name: {0}", cn.Database); Console.WriteLine("Timeout: {0}", cn.ConnectionTimeout); Console.WriteLine("Connection state: {0}\n", cn.State.ToString());
}
While most of these properties are self-explanatory, the State property is worth special mention. Although this property may be assigned any value of the ConnectionState enumeration
public enum System.Data.ConnectionState
{
Broken, Closed, Connecting, Executing, Fetching, Open
}
the only valid ConnectionState values are ConnectionState.Open and ConnectionState.Closed (the remaining members of this enum are reserved for future use). Also, understand that it is always safe to close a connection whose connection state is currently ConnectionState.Closed.
Working with .NET 2.0 ConnectionStringBuilders
Working with connection strings programmatically can be a bit clunky, given that they are often represented as string literals, which are difficult to maintain and error-prone at best. Under .NET 2.0, the Microsoft-supplied ADO.NET data providers now support connection string builder objects, which allow you to establish the name/value pairs using strongly typed properties. Consider the following update to the current Main() method:
static void Main(string[] args)
{
// Create a connection string via the builder object.
SqlConnectionStringBuilder cnStrBuilder = new SqlConnectionStringBuilder();
cnStrBuilder.UserID = "sa"; cnStrBuilder.Password = ""; cnStrBuilder.InitialCatalog = "Cars"; cnStrBuilder.DataSource = "(local)"; cnStrBuilder.ConnectTimeout = 30;
SqlConnection cn = new SqlConnection(); cn.ConnectionString = cnStrBuilder.ConnectionString; cn.Open();

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
781 |
ShowConnectionStatus(cn);
...
}
In this iteration, you create an instance of SqlConnectionStringBuilder, set the properties accordingly, and obtain the internal string via the ConnectionString property. Also note that you make use of the default constructor of the type. If you so choose, you can also create an instance of your data provider’s connection string builder object by passing in an existing connection string as a starting point (which can be helpful when you are reading these values dynamically from an app.config file). Once you have hydrated the object with the initial string data, you can change specific name/value pairs using the related properties, for example:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Data Readers *****\n");
// Assume you really obtained cnStr from a *.config file. string cnStr = "uid=sa;pwd=;Initial Catalog=Cars;" +
"Data Source=(local);Connect Timeout=30";
SqlConnectionStringBuilder cnStrBuilder = new SqlConnectionStringBuilder(cnStr);
cnStrBuilder.UserID = "sa"; cnStrBuilder.Password = ""; cnStrBuilder.InitialCatalog = "Cars"; cnStrBuilder.DataSource = "(local)";
// Change timeout value. cnStrBuilder.ConnectTimeout = 5;
...
}
Working with Command Objects
Now that you better understand the role of the connection object, the next order of business is to check out how to submit SQL queries to the database in question. The SqlCommand type (which derives from DbCommand) is an OO representation of a SQL query, table name, or stored procedure. The type of command is specified using the CommandType property, which may take any value from the CommandType enum:
public enum System.Data.CommandType
{
StoredProcedure,
TableDirect,
Text // Default value.
}
When creating a command object, you may establish the SQL query as a constructor parameter or directly via the CommandText property. Also when you are creating a command object, you need to specify the connection to be used. Again, you may do so as a constructor parameter or via the Connection property:
static void Main(string[] args)
{
SqlConnection cn = new SqlConnection();
...
// Create command object via ctor args.

782 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
string strSQL = "Select * From Inventory"; SqlCommand myCommand = new SqlCommand(strSQL, cn);
// Create another command object via properties.
SqlCommand testCommand = new SqlCommand(); testCommand.Connection = cn; testCommand.CommandText = strSQL;
...
}
Realize that at this point, you have not literally submitted the SQL query to the Cars database, but rather prepped the state of the command type for future use. Table 22-7 highlights some additional members of the DbCommand type.
Table 22-7. Members of the DbCommand Type
Member |
Meaning in Life |
CommandTimeout |
Gets or sets the time to wait while executing the command before |
|
terminating the attempt and generating an error. The default is 30 |
|
seconds. |
Connection |
Gets or sets the DbConnection used by this instance of the DbCommand. |
Parameters |
Gets the collection of DbParameter types used for a parameterized query. |
Cancel() |
Cancels the execution of a command. |
ExecuteReader() |
Returns the data provider’s DbDataReader object, which provides |
|
forward-only, read-only access to the underlying data. |
ExecuteNonQuery() |
Issues the command text to the data store. |
ExecuteScalar() |
A lightweight version of the ExecuteNonQuery() method, designed |
|
specifically for singleton queries (such as obtaining a record count). |
ExecuteXmlReader() |
Microsoft SQL Server (2000 and higher) is capable of returning result |
|
sets as XML. As you might suspect, this method returns |
|
a System.Xml.XmlReader that allows you to process the incoming stream |
|
of XML. |
Prepare() |
Creates a prepared (or compiled) version of the command on the data |
|
source. As you may know, a prepared query executes slightly faster and |
|
is useful when you wish to execute the same query multiple times. |
|
|
|
|
■Note As illustrated later in this chapter, as of .NET 2.0, the SqlCommand object has been updated with additional members that facilitate asynchronous database interactions.
Working with Data Readers
Once you have established the active connection and SQL command, the next step is to submit the query to the data source. As you might guess, you have a number of ways to do so. The DbDataReader type (which implements IDataReader) is the simplest and fastest way to obtain information from a data store. Recall that data readers represent a read-only, forward-only stream of data returned one record at a time. Given this, it should stand to reason that data readers are useful only when submitting SQL selection statements to the underlying data store.
Data readers are useful when you need to iterate over large amounts of data very quickly and have no need to maintain an in-memory representation. For example, if you request 20,000 records from a table to store in a text file, it would be rather memory-intensive to hold this information in

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
783 |
a DataSet. A better approach is to create a data reader that spins over each record as rapidly as possible. Be aware, however, that data reader objects (unlike data adapter objects, which you’ll examine later) maintain an open connection to their data source until you explicitly close the session.
Data reader objects are obtained from the command object via a call to ExecuteReader(). When invoking this method, you may optionally instruct the reader to automatically close down the related connection object by specifying CommandBehavior.CloseConnection.
The following use of the data reader leverages the Read() method to determine when you have reached the end of your records (via a false return value). For each incoming record, you are making use of the type indexer to print out the make, pet name, and color of each automobile. Also note that you call Close() as soon as you are finished processing the records, to free up the connection object:
static void Main(string[] args)
{
...
//Obtain a data reader a la ExecuteReader().
SqlDataReader myDataReader;
myDataReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
//Loop over the results.
while (myDataReader.Read())
{
Console.WriteLine("-> Make: {0}, PetName: {1}, Color: {2}.", myDataReader["Make"].ToString().Trim(), myDataReader["PetName"].ToString().Trim(), myDataReader["Color"].ToString().Trim());
}
myDataReader.Close();
ShowConnectionStatus(cn);
}
■Note The trimming of the string data shown here is only used to remove trailing blank spaces in the database entries; it is not directly related to ADO.NET!
The indexer of a data reader object has been overloaded to take either a string (representing the name of the column) or an integer (representing the column’s ordinal position). Thus, you could clean up the current reader logic (and avoid hard-coded string names) with the following update (note the use of the FieldCount property):
while (myDataReader.Read())
{
Console.WriteLine("***** Record *****");
for (int i = 0; i < myDataReader.FieldCount; i++)
{
Console.WriteLine("{0} = {1} ", myDataReader.GetName(i), myDataReader.GetValue(i).ToString().Trim());
}
Console.WriteLine();
}
If you compile and run your project, you should be presented with a list of all automobiles in the Inventory table of the Cars database (see Figure 22-7).