146 Microsoft ADO.NET 4 Step by Step Along those same generic lines, the SqlDataReader object’s GetSchemaTable method returns a DataTable instance that describes the structure of the queried data. The new table’s content includes columns such as ColumnName, IsKey, and DataTypeName, plus about two dozen more that you can use to understand the makeup of the incoming data. See the Visual Studio online help entry for “SqlDataReader.GetSchemaTable Method” for more information about this method. Processing More Complicated Results SQL Server supports returning multiple record sets in a single query. You can generate them by sending a batch of two or more semicolon-delimited SELECT statements within a single SqlCommand object’s command text, or by executing a stored procedure that generates multiple selections. When retrieving multiple record sets, the returned SqlDataReader initially refers to the first set of records. To access the second set, call the reader’s NextResult method. The method returns False after it passes the final results set. Just as with the reader’s view of individual data rows, SqlDataReader cannot return to an earlier results set. Note The OLE DB and ODBC providers also support nested results, where a single row might contain subordinate data rows. The SQL Server provider does not support nested sets. If you prefer to process the data returned from the query as XML, use the SqlCommand object’s ExecuteXmlReader method (or the asynchronous BeginExecuteXmlReader and EndExecuteXmlReader methods), which returns a System.Xml.XmlReader instance. Your query must include the appropriate XML-specific keywords (such as FOR XML), or it must return valid XML content, such as from a table field. Processing Database Queries: C# 1. Open the “Chapter 9 CSharp” project from the installed samples folder. The project in- cludes a Windows.Forms class named StateBuilder and a sealed class named General. 2. Open the code for the General class. This class centralizes much of the database func- tionality for the sample application. Locate the GetConnectionString function, a routine that uses a SqlConnectionStringBuilder to create a valid connection string to the sample database. It currently includes the following statements: builder.DataSource = @"(local)\SQLExpress"; builder.InitialCatalog = "StepSample"; builder.IntegratedSecurity = true; Adjust these statements as needed to provide access to your own test database. Chapter 9 Querying Databases 147 3. Locate the ExecuteSQL method. This routine processes a SQL statement (sqlText ) on a connected database (linkToDB), expecting no returned results. Within the try block, add the following code: SqlCommand commandWrapper = new SqlCommand(sqlText, linkToDB); commandWrapper.ExecuteNonQuery(); 4. Locate the ExecuteSQLReturn method. This routine processes a SQL statement (sqlText) on a connected database (linkToDB), collecting a single return value from the database and returning it to the calling code. Within the try block, add the following statements: SqlCommand commandWrapper = new SqlCommand(sqlText, linkToDB); return commandWrapper.ExecuteScalar(); 5. Locate the O penReader method. This function processes a SQL statement (sqlText) on a connected database (linkToDB), creating a SqlDataReader object to process the re- turned data rows. Within the try block, add the following lines: SqlCommand commandWrapper = new SqlCommand(sqlText, linkToDB); return commandWrapper.ExecuteReader(); 6. Open the source code view for the StateBuilder form. Locate the RefreshEverything routine. Just after the “See if a custom state already exists” comment, add the following code: sqlText = "SELECT * FROM StateRegion WHERE RegionType = 99"; stateReader = General.OpenReader(sqlText, linkToDB); if ((stateReader != null) && (stateReader.HasRows == true)) { // Existing custom state record. stateReader.Read(); ActiveStateID = (long)(int)stateReader["ID"]; AddName.Text = (string)stateReader["FullName"]; AddAbbreviation.Text = (string)stateReader["Abbreviation"]; } else { // No custom state record. AddName.Clear(); AddAbbreviation.Clear(); } if (stateReader != null) stateReader.Close(); This code uses the Gener al.OpenReader function from step 5 to obtain a SqlD ataReader instance built from a SQL statement (sqlText) and a connection (linkToD B). If the reader contains at least one row, the code accesses specific fields in that first row to populate various internal and onscreen values. 148 Microsoft ADO.NET 4 Step by Step 7. Run the program, a simple database application that lets you create, modify, and re- move a single “state” record. On the Add A State tab, enter New C Sharp in the New State Name field and add CS in the New Abbreviation field. The SQL statement that will add the new record to the StateRe gion table appears just below the edit fields. Click Add to create the record. 8. Use the Rename A State tab to make changes to the test record. When you are finished with the record, use the Delete A State tab to remove the test record. Processing Database Queries: Visual Basic 1. Open the “Chapter 9 VB” project from the installed samples folder. The project includes a Windo ws.Forms class named StateBuilder and a module named General. 2. Open the code for the General module. This file centralizes much of the database func- tionality for the sample application. Locate the GetConnectionString function, a routine that uses a SqlConnectionStringBuilder to create a valid connection string to the sample database. It currently includes the following statements: builder.DataSource = "(local)\SQLExpress" builder.InitialCatalog = "StepSample" builder.IntegratedSecurity = True Adjust these statements as needed to provide access to your own test database. 3. Locate the ExecuteSQL method. This routine processes a SQL statement (sqlText ) on a connected database (linkToDB), expecting no returned results. Within the Try block, add the following code: Dim commandWrapper As New SqlCommand(sqlText, linkToDB) commandWrapper.ExecuteNonQuery() Chapter 9 Querying Databases 149 4. Locate the ExecuteSQLReturn method. This routine processes a SQL statement (sqlText) on a connected database (linkToDB), collecting a single return value from the database and returning it to the calling code. Within the Try block, add the following statements: Dim commandWrapper As New SqlCommand(sqlText, linkToDB) Return commandWrapper.ExecuteScalar() 5. Locate the O penReader method. This function processes a SQL statement (sqlText) on a connected database (linkToDB), creating a SqlDataReader object to process the re- turned data rows. Within the Try block, add the following lines: Dim commandWrapper As New SqlCommand(sqlText, linkToDB) Return commandWrapper.ExecuteReader() 6. Open the source code view for the StateBuilder form. Locate the RefreshEverything routine. Just after the “See if a custom state already exists” comment, add the following code: sqlText = "SELECT * FROM StateRegion WHERE RegionType = 99" stateReader = OpenReader(sqlText, linkToDB) If (stateReader IsNot Nothing) AndAlso (stateReader.HasRows = True) Then ' Existing custom state record. stateReader.Read() ActiveStateID = CLng(stateReader!ID) AddName.Text = CStr(stateReader!FullName) AddAbbreviation.Text = CStr(stateReader!Abbreviation) Else ' No custom state record. AddName.Clear() AddAbbreviation.Clear() End If If (stateReader IsNot Nothing) Then stateReader.Close() This code uses the OpenReader function from step 5 to obtain a SqlDataReader instance built from a SQL statement (sqlText) and a connection (linkToDB). If the reader contains at least one row, the code accesses specific fields in that first row to populate various internal and onscreen values. 7. Run the program, a simple database application that lets you create, modify, and re- move a single “state” record. On the Add A State tab, enter North Visual Basic in the New State Name field and add VB in the New Abbreviation field. The SQL statement that will add the new record to the StateRegion table appears just below the edit fields. Click Add to create the record. 150 Microsoft ADO.NET 4 Step by Step 8. Use the Rename A State tab to make changes to the test record. When you are finished with the record, use the Delete A State tab to remove the test record. Summary This chapter introduced methods for issuing commands to an ADO.NET connected database, and using those commands to retrieve individual or tabular results. The core of this function- ality is the SqlClient.SqlCommand class, a wrapper for SQL Server queries. It includes a variety of methods that process the contained query, optionally returning either a single value or a set of data rows. The SqlDataReader class provides the row-scanning functionality for results retrieved as a data reader. Use the reader’s various Get methods or the default Item property to retrieve field values on each scanned row. When finished with a SqlDataReader, always call its Close or Dispose method. Chapter 9 Querying Databases 151 Chapter 9 Quick Reference To Do This Run a SQL query over an ADO.NET connection Create a SqlCommand instance. Set its Co mmandText property to the SQL statement. Set its Co nnection property to a valid Sql Conne ction instance. Call the command object’s ExecuteNonQuery method. Call a SQL Server stored procedure that returns a single static result Create a SqlCommand instance. Set its Co mmandText property to the stored procedure name, followed by space-delimited arguments if needed. Set its Co nnection property to a valid Sql Conne ction instance. Call the command object’s ExecuteScalar method, capturing the return value. Retrieve two sets of data rows from a SQL Server batch query Create a SqlCommand instance. Set its CommandText property to the semicolon-delimited SQL statements. Set its Co nnection property to a valid Sql Conne ction instance. Call the command object’s ExecuteReader method, assigning the return value to a SqlDataReader variable. Use the reader’s Read method to access rows in the batch’s first set of rows. Call the reader’s NextResult method to access additional results sets. 153 Chapter 10 Adding Standards to Queries After completing this chapter, you will be able to: Understand why parameters are important in queries Add parameters to standard selection and data update queries Call stored procedures that include both in and out parameters In ADO.NET, queries pass to external data sources as strings. These strings include not only essential command keywords and syntactical elements but also the data values used to limit and fulfill each query. Building command strings is an art long practiced by developers in many programming languages, but it’s quite different from .NET’s promise of strongly typed data management. Why store values as distinct data types at all if you are eventually going to convert everything to ASCII text? To push aside these and other deficiencies that stem from inserting all types of data values into SQL statements, ADO.NET includes the parameter, an object that bridges the gap be- tween the text-based needs of the external data source’s command processing system and the intelligent data type system that epitomizes .NET development. This chapter demon- strates query parameters and their uses in SQL Server database queries. Note This chapter focuses on parameters as implemented in the SQL Server provider. Although the OLE DB and ODBC providers also implement parameters, there are some minor differences that will be pointed out within the chapter. The exercises in this chapter all use the same sample project, a tool that uses parameters to re- trieve and update database values. Although you can run the application after each exercise, the expected results for the full application might not appear until you complete all exercises in the chapter. Developing Parameterized Queries In the SQL Server provider, parameters appear as the System.Data.SqlClient.SqlParameter class. By creating relevant parameters and attaching them to SqlCommand instances, ordi- nary text queries become parameterized queries. 154 Microsoft ADO.NET 4 Step by Step Note In the OLE DB provider, the parameter class appears as System.Data.OleDb. OleDbParameter. The ODBC equivalent is System.Data.Odbc.OdbcParameter. Both of these classes and the SqlParameter class in the SQL Server provider derive from System.Data.Common. DbParameter. Understanding the Need for Parameters As mentioned in the “Connection String Builders” section on page 124 of Chapter 8, “Establishing External Connections,” there are certain risks involved in building SQL state- ments and related string elements. A key risk is the SQL injection attack, in which a user can inadvertently or deliberately alter the intent of a SQL statement by supplying corrupted content. Consider the following statement, which modifies the Employee.Salary value for a specific employee record: UPDATE Employee SET Salary = XXX WHERE ID = 5; It works well if the user provides 50000 or a similar number as the value of XXX. But what if resourceful employee John Doe replaces XXX with the following SQL fragments? 150000 WHERE FirstName = 'John' AND LastName = 'Doe'; UPDATE Employee SET Salary = 50000 The user-supplied content includes a semicolon, effectively turning one statement into a batch of two statements. Most programmers design their code to avoid such scenarios, but this type of situation still manages to show up from time to time. Parameters help reduce such issues by using typed substitution placeholders instead of unchecked plain-text gaps in SQL strings. Parameters understand how to properly format their replacement values so that SQL injection attacks and other mishaps don’t occur. Parameters solve these problems by making changes to both the SQL statement and the data destined for that statement. Instead of piecing together workable SQL statements from a combination of programmer and user-supplied parts, parameterized query statements ex- ist in a standardized form, free of unknown and unsafe user data. Portions of the statement that require user input exist as named placeholders, @name elements that get replaced with the final type-specific data values after they have been transmitted to the database. Chapter 10 Adding Standards to Queries 155 This process provides for a more generic command text, and a logical separation between the command and its data. Removing ever-changing data values from SQL statements also increases performance within SQL Server. Like many advanced relational database systems, SQL Server compiles each state- ment into an internal format, one that doesn’t require it to constantly parse a text string to determine its actions. If SQL Server encounters the same SQL statement twice, it doesn’t need to go through the time-consuming compilation process again. For example, the follow- ing three SQL statements are different in the compiler’s view: UPDATE Employee SET Salary = 50000 WHERE ID = 5; UPDATE Employee SET Salary = 56000 WHERE ID = 12; UPDATE Employee SET Salary = 52000 WHERE ID = 8; Parameterized queries replace these three instance-specific versions with a generic version of the statement, free of the varying data portions. Removing dynamic data values from what would otherwise be standard SQL command structures allows applications to send a much more limited number of queries to SQL Server, queries that show up again and again, and that don’t need to be recompiled every time. Implementing Standard Queries The UPDATE statement shown previously modifies the salary for an employee record based on that record’s primary key. UPDATE Employee SET Salary = 50000 WHERE ID = 25; To prepare the statement for parameters, all elements destined for substitution by the parameter values get replaced with “@” identifiers. UPDATE Employee SET Salary = @NewSalary WHERE ID = @EmployeeID; In standard SQL statements (all statements other than stored procedures), the names you provide are up to you, so being descriptive is best. Each placeholder must begin with the @ sign followed by a unique name. Parameter names are not case-sensitive. . the code accesses specific fields in that first row to populate various internal and onscreen values. 148 Microsoft ADO. NET 4 Step by Step 7. Run the program, a simple database application that lets. System.Data.SqlClient.SqlParameter class. By creating relevant parameters and attaching them to SqlCommand instances, ordi- nary text queries become parameterized queries. 1 54 Microsoft ADO. NET 4 Step by Step Note. the expected results for the full application might not appear until you complete all exercises in the chapter. Developing Parameterized Queries In the SQL Server provider, parameters appear