
Pro CSharp 2008 And The .NET 3.5 Platform [eng]
.pdf
762 CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER
Figure 22-15. Fun with data reader objects
Obtaining Multiple Result Sets Using a Data Reader
Data reader objects are able to obtain multiple result sets using a single command object. For example, if you are interested in obtaining all rows from the Inventory table as well as all rows from the Customers table, you are able to specify both SQL select statements using a semicolon delimiter:
string strSQL = "Select * From Inventory;Select * from Customers";
Once you obtain the data reader, you are able to iterate over each result set via the NextResult() method. Do be aware that you are always returned the first result set automatically. Thus, if you wish to read over the rows of each table, you will be able to build the following iteration construct:
do
{
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();
}
} while (myDataReader.NextResult());
So, at this point, you should be more aware of the functionality data reader objects bring to the table. Always remember that a data reader can only process SQL Select statements and cannot be used to modify an existing database table via Insert, Update, or Delete requests. To understand how to modify an existing database requires a further investigation of command objects.

CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER |
763 |
■Source Code The AutoLotDataReader project is included under the Chapter 22 subdirectory.
Building a Reusable Data Access Library
As you have just seen, the ExecuteReader() method extracts a data reader object that allows you to examine the results of a SQL Select statement using a forward-only, read-only flow of information. However, when you wish to submit SQL statements that result in the modification of a given table, you will call the ExecuteNonQuery() method of your command object. This single method will perform inserts, updates, and deletes based on the format of your command text.
■Note Technically speaking, a nonquery is a SQL statement that does not return a result set. Thus, Select statements are queries, while Insert, Update, and Delete statements are not. Given this, ExecuteNonQuery() returns an int that represents the number of rows affected, not a new set of records.
To illustrate how to modify an existing database using nothing more than a call to ExecuteNonQuery(), your next goal is to build a custom data access library that will encapsulate the process of operating upon the AutoLot database. In a production-level environment, your ADO.NET logic will almost always be isolated to a .NET *.dll assembly for one simple reason: code reuse! The first examples of this chapter have not done so just to keep focused on the task at hand; however, as you might imagine, it is would be a waste of time to author the same connection logic, the same data reading logic, and the same command logic for every application that needs to interact with the AutoLot database.
By isolating data access logic to a .NET code library, multiple applications using any sort of front end (console based, desktop based, web based, etc.) can reference the library at hand in a language-independent manner. Thus, if you author your data library using C#, other .NET programmers would be able to build a UI in his or her language of choice (VB, C++/CLI, etc.).
In this chapter, our data library (AutoLotDAL.dll) will contain a single namespace (AutoLotConnectedLayer) that interacts with AutoLot using the connected types of ADO.NET. The next chapter will add a new namespace (AutoLotDisconnectionLayer) to this same *.dll that contains types to communicate with AutoLot using the disconnected layer. This library will then be used by numerous applications over the remainder of the text.
To begin, create a new C# Class Library project named AutoLotDAL (short for AutoLot Data Access Layer) and rename your initial C# code file to AutoLotConnDAL.cs. Next, rename your namespace scope to AutoLotConnectedLayer and change the name of your initial class to InventoryDAL, as this class will define various members to interact with the Inventory table of the AutoLot database. Finally, import the following .NET namespaces:
using System;
using System.Collections.Generic; using System.Text;
//We will make use of the SQL server
//provider; however, it would also be
//permissible to make use of the ADO.NET
//factory pattern for greater flexibility. using System.Data;
using System.Data.SqlClient;

764CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER
namespace AutoLotConnectedLayer
{
public class InventoryDAL
{
}
}
■Note Recall from Chapter 8 that when objects make use of types managing raw resources (such as a database connection), it is a good practice to implement IDisposable and author a proper finalizer. In a production environment, classes such as InventoryDAL would do the same; however, I’ll avoid doing so to stay focused on the particulars of ADO.NET.
Adding the Connection Logic
The first task we must attend to is to define some methods that allow the caller to connect to and disconnect from the data source using a valid connection string. Because our AutoLotDAL.dll assembly will be hard-coded to make use of the types of System.Data.SqlClient, define a private member variable of SqlConnection that is allocated at the time the InventoryDAL object is created. As well, define a method named OpenConnection() and another named CloseConnection() that interact with this member variable as follows:
public class InventoryDAL
{
// This member will be used by all methods. private SqlConnection sqlCn = new SqlConnection();
public void OpenConnection(string connectionString)
{
sqlCn.ConnectionString = connectionString; sqlCn.Open();
}
public void CloseConnection()
{
sqlCn.Close();
}
}
For the sake of brevity, our InventoryDAL type will not test for possible exceptions nor will it throw custom exceptions under various circumstances (such as a malformed connection string). If you were to build an industrial-strength data access library, you would most certainly want to make use of structured exception handling techniques to account for any runtime anomalies.
Adding the Insertion Logic
Inserting a new record into the Inventory table is as simple as formatting the SQL Insert statement (based on user input) and calling the ExecuteNonQuery() using your command object. To illustrate, add a public method to your InventoryDAL type named InsertAuto() that takes four parameters which map to the four columns of the Inventory table (CarID, Color, Make, and PetName). Using the arguments, format a string type to insert the new record. Finally, using your SqlConnection object, execute the SQL statement:

CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER |
765 |
public void InsertAuto(int id, string color, string make, string petName)
{
// Format and execute SQL statement.
string sql = string.Format("Insert Into Inventory" + "(CarID, Make, Color, PetName) Values" +
"('{0}', '{1}', '{2}', '{3}')", id, make, color, petName);
// Execute using our connection.
using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
cmd.ExecuteNonQuery();
}
}
■Note As you may know, building a SQL statement using string concatenation can be risky from a security point of view (think SQL injection attacks). The preferred way to build command text is using a parameterized query, which I describe shortly.
Adding the Deletion Logic
Deleting an existing record is just as simple as inserting a new record. Unlike the code listing for InsertAuto(), I will show one important try/catch scope that handles the possibility of attempting to delete a car that is currently on order for an individual in the Customers table. Add the following method to the InventoryDAL class type:
public void DeleteCar(int id)
{
// Get ID of car to delete, then do so.
string sql = string.Format("Delete from Inventory where CarID = '{0}'", id);
using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
try
{
cmd.ExecuteNonQuery();
}
catch(SqlException ex)
{
Exception error = new Exception("Sorry! That car is on order!", ex); throw error;
}
}
}
Adding the Updating Logic
When it comes to the act of updating an existing record in the Inventory table, the first obvious question is what exactly do we wish to allow the caller to change? The car’s color? The pet name or make? All of the above? Of course one way to allow the caller complete flexibility is to simply define a method that takes a string type to represent any sort of SQL statement, but that is risky at best. Ideally, we would have a set of methods that allow the caller to update a record in a variety of


CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER |
767 |
parameters as objects, rather than a simple blob of text. Treating SQL queries in a more objectoriented manner not only helps reduce the number of typos (given strongly typed properties), but parameterized queries typically execute much faster than a literal SQL string, in that they are parsed exactly once (rather than each time the SQL string is assigned to the CommandText property). Furthermore, parameterized queries also help protect against SQL injection attacks (a well-known data access security issue).
To support parameterized queries, ADO.NET command objects maintain a collection of individual parameter objects. By default, this collection is empty, but you are free to insert any number of parameter objects that map to a “placeholder parameter” in the SQL query. When you wish to associate a parameter within a SQL query to a member in the command object’s parameters collection, prefix the SQL text parameter with the @ symbol (at least when using Microsoft SQL Server; not all DBMSs support this notation).
Specifying Parameters Using the DbParameter Type
Before you build a parameterized query, let’s get to know the DbParameter type (which is the base class to a provider’s specific parameter object). This class maintains a number of properties that allow you to configure the name, size, and data type of the parameter, as well as other characteristics such as the parameter’s direction of travel. Table 22-7 describes some key properties of the
DbParameter type.
Table 22-7. Key Members of the DbParameter Type
Property |
Meaning in Life |
DbType |
Gets or sets the native data type from the data source, represented as a CLR |
|
data type |
Direction |
Gets or sets whether the parameter is input-only, output-only, bidirectional, or a |
|
return value parameter |
IsNullable |
Gets or sets whether the parameter accepts null values |
ParameterName |
Gets or sets the name of the DbParameter |
Size |
Gets or sets the maximum parameter size of the data (only truly useful for |
|
textual data) |
Value |
Gets or sets the value of the parameter |
|
|
To illustrate how to populate a command object’s collection of DBParameter-compatible objects, let’s rework the previous InsertAuto() method to make use of parameter objects (a similar reworking could also be performed for your remaining SQL-centric methods; however, it is not necessary for this example):
public void InsertAuto(int id, string color, string make, string petName)
{
// Note the "placeholders" in the SQL query.
string sql = string.Format("Insert Into Inventory" + "(CarID, Make, Color, PetName) Values" + "(@CarID, @Make, @Color, @PetName)");
// This command will have internal parameters. using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))
{
// Fill params collection.
SqlParameter param = new SqlParameter(); param.ParameterName = "@CarID";

768 CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER
param.Value = id; param.SqlDbType = SqlDbType.Int; cmd.Parameters.Add(param);
param = new SqlParameter(); param.ParameterName = "@Make"; param.Value = make; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param);
param = new SqlParameter(); param.ParameterName = "@Color"; param.Value = color; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param);
param = new SqlParameter(); param.ParameterName = "@PetName"; param.Value = petName; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param);
cmd.ExecuteNonQuery();
}
}
Again, notice that our SQL query consists of four embedded placeholder symbols, each of which is prefixed with the @ token. Using the SqlParameter type, we are able to map each placeholder using the ParameterName property and specify various details (its value, data type, size, etc.) in a strongly typed matter. Once each parameter object is hydrated, it is added to the command object’s collection via a call to Add().
■Note Here, I made use of various properties to establish a parameter object. Do know, however, that parameter objects support a number of overloaded constructors that allow you to set the values of various properties (which will result in a more compact code base). Also be aware that Visual Studio 2008 provides numerous graphical designers that will generate a good deal of this grungy parameter-centric code on your behalf (see Chapter 23).
While building a parameterized query often requires a larger amount of code, the end result is a more convenient way to tweak SQL statements programmatically as well as better overall performance. While you are free to make use of this technique whenever a SQL query is involved, parameterized queries are most helpful when you wish to trigger a stored procedure.
Executing a Stored Procedure
Recall that a stored procedure is a named block of SQL code stored in the database. Stored procedures can be constructed to return a set of rows or scalar data types and may take any number of optional parameters. The end result is a unit of work that behaves like a typical function, with the obvious difference of being located on a data store rather than a binary business object. Currently our AutoLot database defines a single stored procedure named GetPetName, which was formatted as follows:

CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER |
769 |
CREATE PROCEDURE GetPetName @carID int,
@petName char(10) output AS
SELECT @petName = PetName from Inventory where CarID = @carID
Now, consider the following final method of the InventoryDAL type, which invokes our stored procedure:
public string LookUpPetName(int carID)
{
string carPetName = string.Empty;
// Establish name of stored proc.
using (SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn))
{
cmd.CommandType = CommandType.StoredProcedure;
// Input param.
SqlParameter param = new SqlParameter(); param.ParameterName = "@carID"; param.SqlDbType = SqlDbType.Int; param.Value = carID;
param.Direction = ParameterDirection.Input; cmd.Parameters.Add(param);
// Output param.
param = new SqlParameter(); param.ParameterName = "@petName"; param.SqlDbType = SqlDbType.Char; param.Size = 10;
param.Direction = ParameterDirection.Output; cmd.Parameters.Add(param);
//Execute the stored proc. cmd.ExecuteNonQuery();
//Return output param.
carPetName = ((string)cmd.Parameters["@petName"].Value).Trim();
}
return carPetName;
}
The first important aspect of invoking a stored procedure is to recall that a command object can represent a SQL statement (the default) or the name of a stored procedure. When you wish to inform a command object that it will be invoking a stored procedure, you pass in the name of the procedure (as a constructor argument or via the CommandText property) and must set the CommandType property to the value CommandType.StoredProcedure (if you fail to do so, you will receive a runtime exception, as the command object is expecting a SQL statement by default):
SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn); cmd.CommandType = CommandType.StoredProcedure;
Next, notice that the Direction property of a parameter object allows you to specify the direction of travel for each parameter passed to the stored procedure (e.g., input parameters and the output parameter). As before, each parameter object is added to the command object’s parameters collection:

770 CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER
// Input param.
SqlParameter param = new SqlParameter(); param.ParameterName = "@carID"; param.SqlDbType = SqlDbType.Int; param.Value = carID;
param.Direction = ParameterDirection.Input; cmd.Parameters.Add(param);
Finally, once the stored procedure completes via a call to ExecuteNonQuery(), you are able to obtain the value of the output parameter by investigating the command object’s parameter collection and casting accordingly:
// Return output param.
carPetName = (string)cmd.Parameters["@petName"].Value;
■Source Code The AutoLotDAL project is included under the Chapter 22 subdirectory.
Creating a Console UI–Based Front End
At this point, our first iteration of the AutoLotDAL.dll data access library is complete. Using this assembly, we can build any sort of front end to display and edit our data (console based, Windows Forms based, Windows Presentation Foundation applications, or an HTML-based web application). Given that we have not yet examined how to build graphical user interfaces, we will test our data library from a new Console Application named AutoLotCUIClient. Once you create your new project, be sure to add a reference to your AutoLotDAL.dll assembly as well as System.Configuration. dll and update your using statements as follows:
using AutoLotConnectedLayer; using System.Configuration; using System.Data;
Next, insert a new App.config file into your project that contains a <connectionString> element used to connect to your instance of the AutoLot database, for example:
<configuration>
<connectionStrings>
<add name ="AutoLotSqlProvider" connectionString = "Data Source=(local)\SQLEXPRESS;" +
"Integrated Security=SSPI;Initial Catalog=AutoLot"/> </connectionStrings>
</configuration>
Implementing the Main() Method
The Main() method is responsible for prompting the user for a specific course of action and executing that request via a switch statement. This program will allow the user to enter the following commands:
•I: Inserts a new record into the Inventory table
•U: Updates an existing record in the Inventory table
•D: Deletes an existing record from the Inventory table

CHAPTER 22 ■ ADO.NET PART I: THE CONNECTED LAYER |
771 |
•L: Displays the current inventory using a data reader
•S: Shows these options to the user
•P: Look up pet name from car ID
•Q: Quits the program
Each possible option is handled by a unique static method within the Program class. Here is the complete implementation of Main(). Notice that each method invoked from the do/while loop (with the exception of the ShowInstructions() method) takes an InventoryDAL object as its sole parameter:
static void Main(string[] args)
{
Console.WriteLine("***** The AutoLot Console UI *****\n");
//Get connection string from App.config. string cnStr =
ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString; bool userDone = false;
string userCommand = "";
//Create our InventoryDAL object.
InventoryDAL invDAL = new InventoryDAL(); invDAL.OpenConnection(cnStr);
// Keep asking for input until user presses the Q key. try
{
ShowInstructions(); do
{
Console.Write("Please enter your command: "); userCommand = Console.ReadLine(); Console.WriteLine();
switch (userCommand.ToUpper())
{
case "I": InsertNewCar(invDAL); break;
case "U": UpdateCarPetName(invDAL); break;
case "D": DeleteCar(invDAL); break;
case "L": ListInventory(invDAL); break;
case "S": ShowInstructions(); break;
case "P": LookUpPetName(invDAL); break;
case "Q": userDone = true; break;
default: