In the Microsoft .NET Framework, access to a wide variety of data sources is enabled through a group of classes collectively named Microsoft ADO.NET. Each type of data source is supported through the provision of a data provider. Each data provider contains a set of classes that not only implement a standard set of interfaces (defined in the System.Data
namespace), but also provide functionality unique to the data source they support. These classes include representations of connections, commands, properties, data adapters, and data readers through which you interact with a data source.
Table 9-1 lists the data providers included as standard with the .NET Framework.
Table 9.1. .NET Framework Data Provider Implementations
Data Provider | Description |
---|---|
Provides connectivity (via COM Interop) to any data source that implements an ODBC interface. This includes Microsoft SQL Server, Oracle, and Microsoft Access databases. Data provider classes are contained in the | |
Provides connectivity (via COM Interop) to any data source that implements an OLE DB interface. This includes Microsoft SQL Server, MSDE, Oracle, and Jet databases. Data provider classes are contained in the | |
Provides optimized connectivity to Oracle databases via Oracle client software version 8.1.7 or later. Data provider classes are contained in the | |
Provides optimized connectivity to Microsoft SQL Server version 7 and later (including MSDE) by communicating directly with the SQL Server data source, without the need to use ODBC or OLE DB. Data provider classes are contained in the | |
.NET Compact Framework Data Provider | Provides connectivity to Microsoft SQL Server CE. Data provider classes are contained in the |
Where possible, the recipes in this chapter are programmed against the interfaces defined in the System.Data
namespace. This approach makes it easier to apply the solutions to any database. Adopting this approach in your own code will make it more portable. However, the data provider classes that implement these interfaces often implement additional functionality specific to their own database. Generally, you must trade off portability against access to proprietary functionality when it comes to database code. Recipe 9-10 describes how you can use the System.Data.Common.DbProviderFactory
and associated classes to write code not tied to a specific database implementation.
This chapter describes some of the most commonly used aspects of ADO.NET. The recipes in this chapter describe how to do the following:
Create, configure, open, and close database connections (recipe 9-1)
Employ connection pooling to improve the performance and scalability of applications that use database connections (recipe 9-2)
Create and securely store database connection strings (recipes 9-3 and 9-4)
Execute SQL commands and stored procedures, and use parameters to improve their flexibility (recipes 9-5 and 9-6)
Process the results returned by database queries as either a set of rows or as XML (recipes 9-7 and 9-8)
Execute database operations asynchronously, allowing your main code to continue with other tasks while the database operation executes in the background (recipe 9-9)
Write generic ADO.NET code that can be configured to work against any relational database for which a data provider is available (recipe 9-10)
Discover all instances of SQL Server 2000 and SQL Server 2005 available on a network (recipe 9-11)
Create an in-memory cache and programmatically create a DataSet
(recipes 9-12 and 9-13)
Perform LINQ database queries using a DataSet
, and use entity types (recipes 9-14 and 9-15)
Compare the results of LINQ queries (recipe 9-16)
Unless otherwise stated, the recipes in this chapter have been written to use SQL Server 2008 Express Edition running on the local machine and the Northwind
sample database provided by Microsoft. To run the examples against your own database, ensure the Northwind
sample is installed and update the recipe's connection string to contain the name of your server instead of .sqlexpress
. You can obtain the script to set up the Northwind
database from the Microsoft web site. On that site, search for the file named SQL2000SampleDb.msi
to find links to where the file is available for download. The download includes a Readme file with instructions on how to run the installation script.
Create a connection object appropriate to the type of database to which you need to connect. All connection objects implement the System.Data.IDbConnection
interface. Configure the connection object by setting its ConnectionString
property. Open the connection by calling the connection object's Open
method.
The first step in database access is to open a connection to the database. The IDbConnection
interface represents a database connection, and each data provider includes a unique implementation. Here is the list of IDbConnection
implementations for the five standard data providers:
System.Data.Odbc.OdbcConnection
System.Data.OleDb.OleDbConnection
System.Data.OracleClient.OracleConnection
System.Data.SqlServerCe.SqlCeConnection
System.Data.SqlClient.SqlConnection
You configure a connection object using a connection string. A connection string is a set of semicolon-separated name/value pairs. You can supply a connection string either as a constructor argument or by setting a connection object's ConnectionString
property before opening the connection. Each connection class implementation requires that you provide different information in the connection string. Refer to the ConnectionString
property documentation for each implementation to see the values you can specify. Possible settings include the following:
The name of the target database server
The name of the database to open initially
Connection timeout values
Connection-pooling behavior (see recipe 9-2)
Authentication mechanisms to use when connecting to secured databases, including provision of a username and password if needed
Once configured, call the connection object's Open
method to open the connection to the database. You can then use the connection object to execute commands against the data source (discussed in recipe 9-3). The properties of a connection object also allow you to retrieve information about the state of a connection and the settings used to open the connection. When you're finished with a connection, you should always call its Close
method to free the underlying database connection and system resources. IDbConnection
extends System.IDisposable
, meaning that each connection class implements the Dispose
method. Dispose
automatically calls Close
, making the using
statement a very clean and efficient way of using connection objects in your code.
The following example demonstrates how to use both the SqlConnection
and OleDbConnection
classes to open a connection to a Microsoft SQL Server Express database running on the local machine that uses integrated Windows security:
using System; using System.Data; using System.Data.SqlClient; using System.Data.OleDb; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_01 { public static void SqlConnectionExample() { // Create an empty SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source=.sqlexpress;" + // local SQL Server instance "Database=Northwind;" + // the sample Northwind DB "Integrated Security=SSPI"; // integrated Windows security // Open the database connection. con.Open();
// Display information about the connection. if (con.State == ConnectionState.Open) { Console.WriteLine("SqlConnection Information:"); Console.WriteLine(" Connection State = " + con.State); Console.WriteLine(" Connection String = " + con.ConnectionString); Console.WriteLine(" Database Source = " + con.DataSource); Console.WriteLine(" Database = " + con.Database); Console.WriteLine(" Server Version = " + con.ServerVersion); Console.WriteLine(" Workstation Id = " + con.WorkstationId); Console.WriteLine(" Timeout = " + con.ConnectionTimeout); Console.WriteLine(" Packet Size = " + con.PacketSize); } else { Console.WriteLine("SqlConnection failed to open."); Console.WriteLine(" Connection State = " + con.State); } // At the end of the using block Dispose() calls Close(). } } public static void OleDbConnectionExample() { // Create an empty OleDbConnection object. using (OleDbConnection con = new OleDbConnection()) { // Configure the OleDbConnection object's connection string. con.ConnectionString = "Provider=SQLOLEDB;" + // OLE DB Provider for SQL Server @"Data Source=.sqlexpress;" + // local SQL Server instance "Initial Catalog=Northwind;" + // the sample Northwind DB "Integrated Security=SSPI"; // integrated Windows security // Open the database connection. con.Open(); // Display information about the connection. if (con.State == ConnectionState.Open) { Console.WriteLine("OleDbConnection Information:"); Console.WriteLine(" Connection State = " + con.State); Console.WriteLine(" Connection String = " + con.ConnectionString); Console.WriteLine(" Database Source = " + con.DataSource); Console.WriteLine(" Database = " + con.Database); Console.WriteLine(" Server Version = " + con.ServerVersion); Console.WriteLine(" Timeout = " + con.ConnectionTimeout); }
else { Console.WriteLine("OleDbConnection failed to open."); Console.WriteLine(" Connection State = " + con.State); } // At the end of the using block Dispose() calls Close(). } } public static void Main() { // Open connection using SqlConnection. SqlConnectionExample(); Console.WriteLine(Environment.NewLine); // Open connection using OleDbConnection. OleDbConnectionExample(); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to use a pool of database connections to improve application performance and scalability.
Configure the connection pool using settings in the connection string of a connection object.
Connection pooling reduces the overhead associated with creating and destroying database connections. Connection pooling also improves the scalability of solutions by reducing the number of concurrent connections a database must maintain. Many of these connections sit idle for a significant portion of their lifetimes. With connection pooling, instead of creating and opening a new connection object whenever you need one, you take an already open connection from the connection pool. When you have finished using the connection, instead of closing it, you return it to the pool and allow other code to use it.
The SQL Server and Oracle data providers encapsulate connection-pooling functionality, which is enabled by default. One connection pool is created for each unique connection string you specify when you open a new connection. Each time you open a new connection with a connection string that you used previously, the connection is taken from the existing pool. Only if you specify a different connection string will the data provider create a new connection pool. You can control some characteristics of your pool using the connection string settings described in Table 9-2.
Once created, a pool exists until your process terminates.
Table 9.2. Connection String Settings That Control Connection Pooling
Setting | Description |
---|---|
| Specifies the maximum time in seconds that a connection is allowed to live in the pool before it's closed. The age of a connection is tested only when the connection is returned to the pool. This setting is useful for minimizing pool size if the pool is not heavily used, and also ensures optimal load balancing is achieved in clustered database environments. The default value is |
| Supported only by the SQL Server data provider. Specifies whether connections are reset as they are taken from the pool. A value of |
| Specifies the maximum number of connections that should be in the pool. Connections are created and added to the pool as required until this value is reached. If a request for a connection is made but there are no free connections, the caller will block until a connection becomes available. The default value is |
| Specifies the minimum number of connections that should be in the pool. On pool creation, this number of connections is created and added to the pool. During periodic maintenance, or when a connection is requested, connections are added to the pool to ensure that the minimum number of connections are available. The default value is |
| Set to |
The following example demonstrates the configuration of a connection pool that contains a minimum of 5 and a maximum of 15 connections. Connections expire after 10 minutes (600 seconds) and are reset each time a connection is obtained from the pool. The example also demonstrates how to use the Pooling
setting to obtain a connection object that is not from a pool. This is useful if your application uses a single long-lived connection to a database.
using System; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_02 { public static void Main() { // Obtain a pooled connection. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" +// local SQL Server instance "Database = Northwind;" + // the sample Northwind DB "Integrated Security = SSPI;" + // integrated Windows security "Min Pool Size = 5;" + // configure minimum pool size "Max Pool Size = 15;" + // configure maximum pool size "Connection Reset = True;" + // reset connections each use "Connection Lifetime = 600"; // set max connection lifetime // Open the database connection. con.Open(); // Access the database . . . // At the end of the using block, the Dispose calls Close, which // returns the connection to the pool for reuse. } // Obtain a nonpooled connection. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" +//local SQL Server instance "Database = Northwind;" + //the sample Northwind DB "Integrated Security = SSPI;" + //integrated Windows security "Pooling = False"; //specify nonpooled connection // Open the database connection. con.Open(); // Access the database . . . // At the end of the using block, the Dispose calls Close, which // closes the nonpooled connection. }
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
The ODBC and OLE DB data providers also support connection pooling, but they do not implement connection pooling within managed .NET classes, and you do not configure the pool in the same way as you do for the SQL Server or Oracle data providers. ODBC connection pooling is managed by the ODBC Driver Manager and configured using the ODBC Data Source Administrator tool in the Control Panel. OLE DB connection pooling is managed by the native OLE DB implementation. The most you can do is disable pooling by including the setting OLE DB Services=-4;
in your connection string.
The SQL Server CE data provider does not support connection pooling, because SQL Server CE supports only a single concurrent connection.
You need to programmatically create or modify a syntactically correct connection string by working with its component parts or parsing a given connection string.
Use the System.Data.Common.DbConnectionStringBuilder
class or one of its strongly typed subclasses that form part of an ADO.NET data provider.
Connection strings are String
objects that contain a set of configuration parameters in the form of name/value pairs separated by semicolons. These configuration parameters instruct the ADO.NET infrastructure how to open a connection to the data source you want to access and how to handle the life cycle of connections to that data source. As a developer, you will often simply define your connection string by hand and store it in a configuration file (see recipe 9-4). However, you may want to build a connection string from component elements entered by a user, or you may want to parse an existing connection string into its component parts so that you can manipulate it programmatically. The DbConnectionStringBuilder
class and the classes derived from it provide both these capabilities.
DbConnectionStringBuilder
is a class used to create connection strings from name/value pairs or to parse connection strings, but it does not enforce any logic on which configuration parameters are valid. Instead, each data provider (except the SQL Server CE data provider) includes a unique implementation derived from DbConnectionStringBuilder
that accurately enforces the configuration rules for a connection string of that type. Here is the list of available DbConnectionStringBuilder
implementations for standard data providers:
System.Data.Odbc.OdbcConnectionStringBuilder
System.Data.OleDb.OleDbConnectionStringBuilder
System.Data.OracleClient.OracleConnectionStringBuilder
System.Data.SqlClient.SqlConnectionStringBuilder
Each of these classes exposes properties for getting and setting the possible parameters for a connection string of that type. To parse an existing connection string, pass it as an argument when creating the DbConnectionStringBuilder
-derived class, or set the ConnectionString
property. If this string contains a keyword not supported by the type of connection, an ArgumentException
will be thrown.
The following example demonstrates the use of the SqlConnectionStringBuilder
class to parse and construct SQL Server connection strings:
using System; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_03 { public static void Main(string[] args) { string conString = @"Data Source=.sqlexpress;" + "Database=Northwind;Integrated Security=SSPI;" + "Min Pool Size=5;Max Pool Size=15;Connection Reset=True;" + "Connection Lifetime=600;"; // Parse the SQL Server connection string and display the component // configuration parameters. SqlConnectionStringBuilder sb1 = new SqlConnectionStringBuilder(conString); Console.WriteLine("Parsed SQL Connection String Parameters:"); Console.WriteLine(" Database Source = " + sb1.DataSource); Console.WriteLine(" Database = " + sb1.InitialCatalog); Console.WriteLine(" Use Integrated Security = " + sb1.IntegratedSecurity); Console.WriteLine(" Min Pool Size = " + sb1.MinPoolSize); Console.WriteLine(" Max Pool Size = " + sb1.MaxPoolSize); Console.WriteLine(" Lifetime = " + sb1.LoadBalanceTimeout);
// Build a connection string from component parameters and display it. SqlConnectionStringBuilder sb2 = new SqlConnectionStringBuilder(conString); sb2.DataSource = @".sqlexpress"; sb2.InitialCatalog = "Northwind"; sb2.IntegratedSecurity = true; sb2.MinPoolSize = 5; sb2.MaxPoolSize = 15; sb2.LoadBalanceTimeout = 600; Console.WriteLine(Environment.NewLine); Console.WriteLine("Constructed connection string:"); Console.WriteLine(" " + sb2.ConnectionString); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Store the connection string in an encrypted section of the application's configuration file.
Protected configuration—the .NET Framework feature that lets you encrypt configuration information—relies on the key storage facilities of the Data Protection API (DPAPI) to store the secret key used to encrypt the configuration file. This solves the very difficult problem of code-based secret key management.
Database connection strings often contain secret information, or at the very least information that would be valuable to someone trying to attack your system. As such, you should not store connection strings in plain text, nor should you hard-code them into the application code. Strings embedded in an assembly can easily be retrieved using a disassembler. The .NET Framework includes a number of classes and capabilities that make storing and retrieving encrypted connection strings in your application's configuration trivial.
Unencrypted connection strings are stored in the machine or application configuration file in the <connectionStrings>
section in the format shown here:
<configuration> <connectionStrings> <add name="ConnectionString1" connectionString="Data Source=.sqlexpress ;Database=Northwind;Integrated Security=SSPI;Min Pool Size=5;Max Pool Size=15;Co nnection Reset=True;Connection Lifetime=600;" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration>
The easiest way to read this connection string is to use the indexed ConnectionStrings
property of the System.Configuration.ConfigurationManager
class. Specifying the name of the connection string you want as the property index will return a System.Configuration.ConnectionStringSettings
object. The ConnectionStringSettings.ConnectionString
property gets the connection string, and the ConnectionStringSettings.ProviderName
property gets the provider name that you can use to create a data provider factory (see recipe 9-10). This process will work regardless of whether the connection string has been encrypted or written in plain text.
To write a connection string to the application's configuration file, you must first obtain a System.Configuration.Configuration
object, which represents the application's configuration file. The easiest way to do this is by calling the System.Configuration.ConfigurationManager.OpenExeConfiguration
method. You should then create and configure a new System.Configuration.ConnectionStringSettings
object to represent the stored connection string. You should provide a name, connection string, and data provider name for storage. Add the ConnectionStringSettings
object to Configuration
's ConnectionStringsSection
collection, available through the Configuration.ConnectionStrings
property. Finally, save the updated file by calling the Configuration.Save
method.
To encrypt the connection strings section of the configuration file, before saving the file, you must configure the ConnectionStringsSection
collection. To do this, call the ConnectionStringsSection
.SectionInformation.ProtectSection
method and pass it a string containing the name of the protected configuration provider to use: either RsaProtectedConfigurationProvider
or DPAPIProtectedConfigurationProvider
. To disable encryption, call the SectionInformation.Unprotect
method.
To use the classes from the System.Configuration
namespace discussed in this recipe, you must add a reference to the System.Configuration.dll
assembly when you build your application.
The following example demonstrates the writing of an encrypted connection string to the application's configuration file and the subsequent reading and use of that connection string.
The configuration file will be created alongside the compiled program in the bin/Release
or bin/Debug
directory of the Visual Studio project folder. If you have downloaded the source code that accompanies this book, the configuration tile will be called Recipe09-04.exe.Config
.
using System; using System.Configuration; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_04 { private static void WriteEncryptedConnectionStringSection( string name, string constring, string provider) { // Get the configuration file for the current application. Specify // the ConfigurationUserLevel.None argument so that we get the // configuration settings that apply to all users. Configuration config = ConfigurationManager.OpenExeConfiguration( ConfigurationUserLevel.None); // Get the connectionStrings section from the configuration file. ConnectionStringsSection section = config.ConnectionStrings; // If the connectionString section does not exist, create it. if (section == null) { section = new ConnectionStringsSection(); config.Sections.Add("connectionSettings", section); } // If it is not already encrypted, configure the connectionStrings // section to be encrypted using the standard RSA Proected // Configuration Provider. if (!section.SectionInformation.IsProtected) { // Remove this statement to write the connection string in clear // text for the purpose of testing. section.SectionInformation.ProtectSection( "RsaProtectedConfigurationProvider"); } // Create a new connection string element and add it to the // connection string configuration section. ConnectionStringSettings cs = new ConnectionStringSettings(name, constring, provider); section.ConnectionStrings.Add(cs);
// Force the connection string section to be saved. section.SectionInformation.ForceSave = true; // Save the updated configuration file. config.Save(ConfigurationSaveMode.Full); } public static void Main(string[] args) { // The connection string information to be written to the // configuration file. string conName = "ConnectionString1"; string conString = @"Data Source=.sqlexpress;" + "Database=Northwind;Integrated Security=SSPI;" + "Min Pool Size=5;Max Pool Size=15;Connection Reset=True;" + "Connection Lifetime=600;"; string providerName = "System.Data.SqlClient"; // Write the new connection string to the application's // configuration file. WriteEncryptedConnectionStringSection(conName, conString, providerName); // Read the encrypted connection string settings from the // application's configuration file. ConnectionStringSettings cs2 = ConfigurationManager.ConnectionStrings["ConnectionString1"]; // Use the connection string to create a new SQL Server connection. using (SqlConnection con = new SqlConnection(cs2.ConnectionString)) { // Issue database commands/queries . . . } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Create a command object appropriate to the type of database you intend to use. All command objects implement the System.Data.IDbCommand
interface. Configure the command object by setting its CommandType
and CommandText
properties. Execute the command using the ExecuteNonQuery, ExecuteReader
, or ExecuteScalar
method, depending on the type of command and its expected results.
The IDbCommand
interface represents a database command, and each data provider includes a unique implementation. Here is the list of IDbCommand
implementations for the five standard data providers:
System.Data.Odbc.OdbcCommand
System.Data.OleDb.OleDbCommand
System.Data.OracleClient.OracleCommand
System.Data.SqlServerCe.SqlCeCommand
System.Data.SqlClient.SqlCommand
To execute a command against a database, you must have an open connection (discussed in recipe 9-1) and a properly configured command object appropriate to the type of database you are accessing. You can create command objects directly using a constructor, but a simpler approach is to use the CreateCommand
factory method of a connection object. The CreateCommand
method returns a command object of the correct type for the data provider and configures it with basic information obtained from the connection you used to create the command. Before executing the command, you must configure the properties described in Table 9-3, which are common to all command implementations.
Table 9.3. Common Command Object Properties
Property | Description |
---|---|
| A |
| An |
| A value of the |
| An |
| A |
| A |
Once you have configured your command object, you can execute it in a number of ways, depending on the nature of the command, the type of data returned by the command, and the format in which you want to process the data.
To execute a command that does not return database data (such as INSERT, DELETE
, or CREATE TABLE
), call ExecuteNonQuery
. For the UPDATE, INSERT
, and DELETE
commands, the ExecuteNonQuery
method returns an int
that specifies the number of rows affected by the command. For other commands, such as CREATE TABLE, ExecuteNonQuery
returns the value −1
.
To execute a command that returns a result set, such as a SELECT
statement or stored procedure, use the ExecuteReader
method. ExecuteReader
returns an IDataReader
instance (discussed in recipe 9-7) through which you have access to the result data. Most data providers also allow you to execute multiple SQL commands in a single call to the ExecuteReader
method, as demonstrated in the example in recipe 9-7, which also shows how to access each result set.
If you want to execute a query but only need the value from the first column of the first row of result data, use the ExecuteScalar
method. The value is returned as an object
reference that you must cast to the correct type.
The IDbCommand
implementations included in the Oracle and SQL data providers implement additional command execution methods. Recipe 9-8 describes how to use the ExecuteXmlReader
method provided by the SqlCommand
class. Refer to the .NET Framework's SDK documentation for details on the additional ExecuteOracleNonQuery
and ExecuteOracleScalar
methods provided by the OracleCommand
class.
The following example demonstrates the use of command objects to update a database record, run a stored procedure, and obtain a scalar value:
using System; using System.Data; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_05 { public static void ExecuteNonQueryExample(IDbConnection con) { // Create and configure a new command. IDbCommand com = con.CreateCommand(); com.CommandType = CommandType.Text; com.CommandText = "UPDATE Employees SET Title = 'Sales Director'" + " WHERE EmployeeId = '5'"; // Execute the command and process the result. int result = com.ExecuteNonQuery(); if (result == 1) { Console.WriteLine("Employee title updated."); } else { Console.WriteLine("Employee title not updated."); } } public static void ExecuteReaderExample(IDbConnection con) { // Create and configure a new command. IDbCommand com = con.CreateCommand(); com.CommandType = CommandType.StoredProcedure; com.CommandText = "Ten Most Expensive Products"; // Execute the command and process the results. using (IDataReader reader = com.ExecuteReader()) { Console.WriteLine("Price of the Ten Most Expensive Products."); while (reader.Read()) { // Display the product details. Console.WriteLine(" {0} = {1}", reader["TenMostExpensiveProducts"], reader["UnitPrice"]); } } }
public static void ExecuteScalarExample(IDbConnection con) { // Create and configure a new command. IDbCommand com = con.CreateCommand(); com.CommandType = CommandType.Text; com.CommandText = "SELECT COUNT(*) FROM Employees"; // Execute the command and cast the result. int result = (int)com.ExecuteScalar(); Console.WriteLine("Employee count = " + result); } public static void Main() { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Open the database connection and execute the example // commands through the connection. con.Open(); ExecuteNonQueryExample(con); Console.WriteLine(Environment.NewLine); ExecuteReaderExample(con); Console.WriteLine(Environment.NewLine); ExecuteScalarExample(con); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to set the arguments of a stored procedure or use parameters in a SQL command to improve flexibility.
Create parameter objects appropriate to the type of command object you intend to execute. All parameter objects implement the System.Data.IDataParameter
interface. Configure the parameter objects' data types, values, and directions, and add them to the command object's parameter collection using the IDbCommand.Parameters.Add
method.
All command objects support the use of parameters, so you can do the following:
Set the arguments of stored procedures.
Receive stored procedure return values.
Substitute values into text commands at runtime.
The IDataParameter
interface represents a parameter, and each data provider includes a unique implementation. Here is the list of IDataParameter
implementations for the five standard data providers:
System.Data.Odbc.OdbcParameter
System.Data.OleDb.OleDbParameter
System.Data.OracleClient.OracleParameter
System.Data.SqlServerCe.SqlCeParameter
System.Data.SqlClient.SqlParameter
To use parameters with a text command, you must identify where to substitute the parameter's value within the command. The ODBC, OLE DB, and SQL Server CE data providers support positional parameters; the location of each argument is identified by a question mark (?
). For example, the following command identifies two locations to be substituted with parameter values:
UPDATE Employees SET Title = ? WHERE EmployeeId = ?
The SQL Server and Oracle data providers support named parameters, which allow you to identify each parameter location using a name preceded by the at symbol (@
). Here is the equivalent command using named parameters:
UPDATE Employees SET Title = @title WHERE EmployeeId = @id
To specify the parameter values to substitute into a command, you must create parameter objects of the correct type and add them to the command object's parameter collection accessible through the Parameters
property. You can add named parameters in any order, but you must add positional parameters in the same order they appear in the text command. When you execute your command, the value of each parameter is substituted into the command string before the command is executed against the data source. You can create parameter objects in the following ways:
Use the IDbCommand.CreateParameter
method.
Use the IDbCommand.Parameters.Add
method.
Use System.Data.Common.DbProviderFactory
.
Directly create parameter objects using constructors and configure them using constructor arguments or through setting their properties. (This approach ties you to a specific database provider.)
A parameter object's properties describe everything about a parameter that the command object needs to use the parameter object when executing a command against a data source. Table 9-4 describes the properties that you will use most frequently when configuring parameters.
Table 9.4. Commonly Used Parameter Properties
Property | Description |
---|---|
| A value of the |
| A value from the |
| A |
| A |
| An |
When using parameters to execute stored procedures, you must provide parameter objects to satisfy each argument required by the stored procedure, including both input and output arguments. You must set the Direction
property of each parameter as described in Table 9-4; parameters are Input
by default. If a stored procedure has a return value, the parameter to hold the return value (with a Direction
property equal to ReturnValue
) must be the first parameter added to the parameter collection.
The following example demonstrates the use of parameters in SQL commands. The ParameterizedCommandExample
method demonstrates the use of parameters in a SQL Server UPDATE
statement. The ParameterizedCommandExample
method's arguments include an open SqlConnection
and two strings. The values of the two strings are substituted into the UPDATE
command using parameters. The StoredProcedureExample
method demonstrates the use of parameters to call a stored procedure.
using System; using System.Data; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_06 { public static void ParameterizedCommandExample(SqlConnection con, string employeeID, string title) { // Create and configure a new command containing two named parameters. using (SqlCommand com = con.CreateCommand()) { com.CommandType = CommandType.Text; com.CommandText = "UPDATE Employees SET Title = @title" + " WHERE EmployeeId = @id"; // Create a SqlParameter object for the title parameter. SqlParameter p1 = com.CreateParameter(); p1.ParameterName = "@title"; p1.SqlDbType = SqlDbType.VarChar; p1.Value = title; com.Parameters.Add(p1); // Use a shorthand syntax to add the id parameter. com.Parameters.Add("@id", SqlDbType.Int).Value = employeeID; // Execute the command and process the result. int result = com.ExecuteNonQuery(); if (result == 1) { Console.WriteLine("Employee {0} title updated to {1}.", employeeID, title); } else { Console.WriteLine("Employee {0} title not updated.", employeeID); } } } public static void StoredProcedureExample(SqlConnection con, string category, string year) {
// Create and configure a new command. using (SqlCommand com = con.CreateCommand()) { com.CommandType = CommandType.StoredProcedure; com.CommandText = "SalesByCategory"; // Create a SqlParameter object for the category parameter. com.Parameters.Add("@CategoryName", SqlDbType.NVarChar).Value = category; // Create a SqlParameter object for the year parameter. com.Parameters.Add("@OrdYear", SqlDbType.NVarChar).Value = year; // Execute the command and process the results. using (IDataReader reader = com.ExecuteReader()) { Console.WriteLine("Sales By Category ({0}).", year); while (reader.Read()) { // Display the product details. Console.WriteLine(" {0} = {1}", reader["ProductName"], reader["TotalPurchase"]); } } } } public static void Main() { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Open the database connection and execute the example // commands through the connection. con.Open(); ParameterizedCommandExample(con, "5", "Cleaner"); Console.WriteLine(Environment.NewLine); StoredProcedureExample(con, "Seafood", "1999"); Console.WriteLine(Environment.NewLine); }
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to process the data contained in a System.Data.IDataReader
instance returned when you execute the IDbCommand.ExecuteReader
method (discussed in recipe 9-5).
Use the members of the IDataReader
instance to move through the rows in the result set sequentially and access the individual data items contained in each row.
The IDataReader
interface represents a data reader, which is a forward-only, read-only mechanism for accessing the results of a SQL query. Each data provider includes a unique IDataReader
implementation. Here is the list of IDataReader
implementations for the five standard data providers:
System.Data.Odbc.OdbcDataReader
System.Data.OleDb.OleDbDataReader
System.Data.OracleClient.OracleDataReader
System.Data.SqlServerCe.SqlCeDataReader
System.Data.SqlClient.SqlDataReader
The IDataReader
interface extends the System.Data.IDataRecord
interface. Together, these interfaces declare the functionality that provides access to both the data and the structure of the data contained in the result set. Table 9-5 describes some of the commonly used members of the IDataReader
and IDataRecord
interfaces.
Table 9.5. Commonly Used Members of Data Reader Classes
Member | Description |
---|---|
Property | |
| Gets the number of columns in the current row. |
| Returns |
| Returns an |
Method | |
| Gets the name of the data source data type for a specified column. |
| Gets a |
| Gets the name of the column specified by using a zero-based integer index. |
| Gets the zero-based column ordinal for the column with the specified name. |
| Returns a |
| Returns |
| Moves to the next set of results if |
| Advances the reader to the next record. The reader always starts prior to the first record. |
In addition to those members listed in Table 9-5, the data reader provides a set of methods for retrieving typed data from the current row. Each of the following methods takes an integer argument that identifies the zero-based index of the column from which the data should be returned: GetBoolean, GetByte, GetBytes, GetChar, GetChars, GetDateTime, GetDecimal, GetDouble, GetFloat, GetGuid, GetInt16, GetInt32, GetInt64, GetString, GetValue
, and GetValues
.
The SQL Server and Oracle data readers also include methods for retrieving data as data source–specific data types. For example, SqlDataReader
includes methods such as GetSqlByte, GetSqlDecimal
, and GetSqlMoney
; and OracleDataReader
includes methods such as GetOracleLob, GetOracleNumber
, and GetOracleMonthSpan
. Refer to the .NET Framework SDK documentation for more details.
When you have finished with a data reader, you should always call its Close
method so that you can use the database connection again. IDataReader
extends System.IDisposable
, meaning that each data reader class implements the Dispose
method. Dispose
automatically calls Close
, making the using
statement a very clean and efficient way of using data readers.
The following example demonstrates the use of a data reader to process the contents of two result sets returned by executing a batch query containing two SELECT
queries. The first result set is enumerated and displayed to the console. The second result set is inspected for metadata information, which is then displayed.
using System; using System.Data; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_07 { public static void Main() { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Create and configure a new command. using (SqlCommand com = con.CreateCommand()) { com.CommandType = CommandType.Text; com.CommandText = "SELECT BirthDate,FirstName,LastName FROM "+ "Employees ORDER BY BirthDate;SELECT * FROM Employees"; // Open the database connection and execute the example. // commands through the connection. con.Open(); // Execute the command and obtain a SqlReader. using (SqlDataReader reader = com.ExecuteReader()) { // Process the first set of results and display the // content of the result set. Console.WriteLine("Employee Birthdays (By Age).");
while (reader.Read()) { Console.WriteLine(" {0,18:D} - {1} {2}", reader.GetDateTime(0), // Retrieve typed data reader["FirstName"], // Use string index reader[2]); // Use ordinal index } Console.WriteLine(Environment.NewLine); // Process the second set of results and display details // about the columns and data types in the result set. reader.NextResult(); Console.WriteLine("Employee Table Metadata."); for (int field = 0; field < reader.FieldCount; field++) { Console.WriteLine(" Column Name:{0} Type:{1}", reader.GetName(field), reader.GetDataTypeName(field)); } } } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Specify the FOR XML
clause in your SQL query to return the results as XML. Execute the command using the ExecuteXmlReader
method of the System.Data.SqlClient.SqlCommand
class, which returns a System.Xml.XmlReader
object through which you can access the returned XML data.
SQL Server 2000 (and later versions) provides direct support for XML. You simply need to add the clause FOR XML AUTO
to the end of a SQL query to indicate that the results should be returned as XML. By default, the XML representation is not a full XML document. Instead, it simply returns the result of each record in a separate element, with all the fields as attributes. For example, the query
SELECT CustomerID, CompanyName FROM Customers FOR XML AUTO
returns XML with the following structure:
<Customers CustomerID="ALFKI" CompanyName="Alfreds Futterkiste"/> <Customers CustomerID="ANTON" CompanyName="Antonio Moreno Taquería"/> <Customers CustomerID="GOURL" CompanyName="Gourmet Lanchonetes"/>
Alternatively, you can add the ELEMENTS
keyword to the end of a query to structure the results using nested elements rather than attributes. For example, the query
SELECT CustomerID, CompanyName FROM Customers FOR XML AUTO, ELEMENTS
returns XML with the following structure:
<Customers> <CustomerID>ALFKI</CustomerID> <CompanyName>Alfreds Futterkiste</CompanyName> </Customers> <Customers> <CustomerID>ANTON</CustomerID> <CompanyName>Antonio Moreno Taquería</CompanyName> </Customers> <Customers> <CustomerID>GOURL</CustomerID> <CompanyName>Gourmet Lanchonetes</CompanyName> </Customers>
You can also fine-tune the format in more detail using the FOR XML EXPLICIT
syntax. For example, this allows you to convert some fields to attributes and others to elements. Refer to SQL Server Books Online for more information.
When the ExecuteXmlReader
command returns, the connection cannot be used for any other commands while XmlReader
is open. You should process the results as quickly as possible, and you must always close XmlReader
. Instead of working with XmlReader
and accessing the data sequentially, you can read the XML data into a System.Xml.XmlDocument
. This way, all the data is retrieved into memory, and the database connection can be closed. You can then continue to interact with the XML document. (Chapter 6 contains numerous examples of how to use the XmlReader
and XmlDocument
classes.)
The following example demonstrates how to retrieve results as XML using the FOR XML
clause and the ExecuteXmlReader
method:
using System; using System.Xml; using System.Data; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_08 { public static void ConnectedExample() { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Create and configure a new command that includes the // FOR XML AUTO clause. using (SqlCommand com = con.CreateCommand()) { com.CommandType = CommandType.Text; com.CommandText = "SELECT CustomerID, CompanyName" + " FROM Customers FOR XML AUTO"; // Open the database connection. con.Open(); // Execute the command and retrieve an XmlReader to access // the results. using (XmlReader reader = com.ExecuteXmlReader()) { while (reader.Read()) { Console.Write("Element: " + reader.Name); if (reader.HasAttributes) { for (int i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); Console.Write(" {0}: {1}", reader.Name, reader.Value); }
// Move the XmlReader back to the element node. reader.MoveToElement(); Console.WriteLine(Environment.NewLine); } } } } } } public static void DisconnectedExample() { XmlDocument doc = new XmlDocument(); // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Create and configure a new command that includes the // FOR XML AUTO clause. SqlCommand com = con.CreateCommand(); com.CommandType = CommandType.Text; com.CommandText = "SELECT CustomerID, CompanyName FROM Customers FOR XML AUTO"; // Open the database connection. con.Open(); // Load the XML data into the XmlDocument. Must first create a // root element into which to place each result row element. XmlReader reader = com.ExecuteXmlReader(); doc.LoadXml("<results></results>"); // Create an XmlNode from the next XML element read from the // reader. XmlNode newNode = doc.ReadNode(reader); while (newNode != null) { doc.DocumentElement.AppendChild(newNode); newNode = doc.ReadNode(reader); } } // Process the disconnected XmlDocument. Console.WriteLine(doc.OuterXml); }
public static void Main(string[] args) { ConnectedExample(); Console.WriteLine(Environment.NewLine); DisconnectedExample(); Console.WriteLine(Environment.NewLine); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to execute a query or command against a SQL Server database as a background task while your application continues with other processing.
Use the BeginExecuteNonQuery, BeginExecuteReader
, or BeginExecuteXmlReader
method of the System.Data.SqlClient.SqlCommand
class to start the database operation as a background task. These methods all return a System.IAsyncResult
object that you can use to determine the operation's status or use thread synchronization to wait for completion. Use the IAsyncResult
object and the corresponding EndExecuteNonQuery, EndExecuteReader
, or EndExecuteXmlReader
method to obtain the result of the operation.
Only the SqlCommand
class supports the asynchronous operations described in this recipe. The equivalent command classes for the Oracle, SQL Server CE, ODBC, and OLE DB data providers do not provide this functionality.
You will usually execute operations against databases synchronously, meaning that the calling code blocks until the operation is complete. Synchronous calls are most common because your code will usually require the result of the operation before it can continue. However, sometimes it's useful to execute a database operation asynchronously, meaning that you start the method in a separate thread and then continue with other operations.
To execute asynchronous operations over a System.Data.SqlClient.SqlConnection
connection, you must specify the value Asynchronous Processing=true
in its connection string.
The SqlCommand
class implements the asynchronous execution pattern similar to that discussed in recipe 4-2. As with the general asynchronous execution pattern described in recipe 4-2, the arguments of the asynchronous execution methods (BeginExecuteNonQuery, BeginExecuteReader
, and BeginExecuteXmlReader
) are the same as those of the synchronous variants (ExecuteNonQuery, ExecuteReader
, and ExecuteXmlReader
), but they take the following two additional arguments to support asynchronous completion:
A System.AsyncCallback
delegate instance that references a method that the runtime will call when the asynchronous operation completes. The method is executed in the context of a thread-pool thread. Passing null
means that no method is called and you must use another completion mechanism (discussed later in this recipe) to determine when the asynchronous operation is complete.
An object
reference that the runtime associates with the asynchronous operation. The asynchronous operation does not use nor have access to this object, but it's available to your code when the operation completes, allowing you to associate useful state information with an asynchronous operation. For example, this object allows you to map results against initiated operations in situations where you initiate many asynchronous operations that use a common callback method to perform completion.
The EndExecuteNonQuery, EndExecuteReader
, and EndExecuteXmlReader
methods allow you to retrieve the return value of an operation that was executed asynchronously, but you must first determine when it has finished. Here are the four techniques for determining if an asynchronous method has finished:
Blocking
: This method stops the execution of the current thread until the asynchronous operation completes execution. In effect, this is much the same as synchronous execution. However, in this case, you have the flexibility to decide exactly when your code enters the blocked state, giving you the opportunity to carry out some additional processing before blocking.
Polling
: This method involves repeatedly testing the state of an asynchronous operation to determine whether it's complete. This is a very simple technique and is not particularly efficient from a processing perspective. You should avoid tight loops that consume processor time. It's best to put the polling thread to sleep for a period using Thread.Sleep
between completion tests. Because polling involves maintaining a loop, the actions of the waiting thread are limited, but you can easily update some kind of progress indicator.
Waiting
: This method uses an object derived from the System.Threading.WaitHandle
class to signal when the asynchronous method completes. Waiting is a more efficient version of polling and in addition allows you to wait for multiple asynchronous operations to complete. You can also specify timeout values to allow your waiting thread to fail if the asynchronous operation takes too long, or if you want to periodically update a status indicator.
Callback
: This a method that the runtime calls when an asynchronous operation completes. The calling code does not need to take any steps to determine when the asynchronous operation is complete and is free to continue with other processing. Callbacks provide the greatest flexibility, but also introduce the greatest complexity, especially if you have many concurrently active asynchronous operations that all use the same callback. In such cases, you must use appropriate state objects to match completed methods against those you initiated.
When using the asynchronous capabilities of the SQL Server data provider, you must ensure that your code does not inadvertently dispose of objects that are still being used by other threads. Pay particular attention to SqlConnection
and SqlCommand
objects.
Recipe 4-2 provides examples of all of the completion techniques summarized in the preceding list. The following example demonstrates the use of an asynchronous call to execute a stored procedure on a SQL Server database. The code uses a callback to process the returned result set.
using System; using System.Data; using System.Threading; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_09 { // A method to handle asynchronous completion using callbacks. public static void CallbackHandler(IAsyncResult result) { // Obtain a reference to the SqlCommand used to initiate the // asynchronous operation. using (SqlCommand cmd = result.AsyncState as SqlCommand) { // Obtain the result of the stored procedure. using (SqlDataReader reader = cmd.EndExecuteReader(result)) {
// Display the results of the stored procedure to the console. lock (Console.Out) { Console.WriteLine( "Price of the Ten Most Expensive Products:"); while (reader.Read()) { // Display the product details. Console.WriteLine(" {0} = {1}", reader["TenMostExpensiveProducts"], reader["UnitPrice"]); } } } } } public static void Main() { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. // You must specify Asynchronous Processing=true to support // asynchronous operations over the connection. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI;" + "Asynchronous Processing=true"; // Create and configure a new command to run a stored procedure. // Do not wrap it in a using statement because the asynchronous // completion handler will dispose of the SqlCommand object. SqlCommand cmd = con.CreateCommand(); cmd.CommandType = CommandType.StoredProcedure; cmd.CommandText = "Ten Most Expensive Products"; // Open the database connection and execute the command // asynchronously. Pass the reference to the SqlCommand // used to initiate the asynchronous operation. con.Open(); cmd.BeginExecuteReader(CallbackHandler, cmd);
// Continue with other processing. for (int count = 0; count < 10; count++) { lock (Console.Out) { Console.WriteLine("{0} : Continue processing...", DateTime.Now.ToString("HH:mm:ss.ffff")); } Thread.Sleep(500); } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
You need to write code that can be configured to work against any relational database supported by an ADO.NET data provider.
Program to the ADO.NET data provider interfaces in the System.Data
namespace, as opposed to the concrete implementations, and do not rely on features and data types that are unique to specific database implementations. Use factory classes and methods to instantiate the data provider objects you need to use.
Using a specific data provider implementation (the SQL Server data provider, for example) simplifies your code, and may be appropriate if you need to support only a single type of database or require access to specific features provided by that data provider, such as the asynchronous execution for SQL Server detailed in recipe 9-9. However, if you program your application against a specific data provider implementation, you will need to rewrite and test those sections of your code if you want to use a different data provider at some point in the future.
Table 9-6 contains a summary of the main interfaces you must program against when writing generic ADO.NET code that will work with any relational database's data provider. The table also explains how to create objects of the appropriate type that implement the interface. Many of the recipes in this chapter demonstrate the use of ADO.NET data provider interfaces over specific implementation, as highlighted in the table.
Table 9.6. Data Provider Interfaces
Interface | Description | Demonstrated In |
---|---|---|
| Represents a connection to a relational database. You must program the logic to create a connection object of the appropriate type based on your application's configuration information, or use the | |
| Represents a SQL command that is issued to a relational database. You can create | |
| Represents a parameter to an | |
| Represents the result set of a database query and provides access to the contained rows and columns. An object of the correct type will be returned when you call the | Recipes 9-5 and 9-6 |
| Represents the set of commands used to fill a |
The System.Data.Common.DbProviderFactory
class provides a set of factory methods for creating all types of data provider objects, making it very useful for implementing generic database code. Most important, DbProviderFactory
provides a mechanism for obtaining an initial IDbConnection
instance, which is the critical starting point for writing generic ADO.NET code. Each of the standard data provider implementations (except the SQL Server CE data provider) includes a unique factory class derived from DbProviderFactory
. Here is the list of DbProviderFactory
subclasses:
System.Data.Odbc.OdbcFactory
System.Data.OleDb.OleDbFactory
System.Data.OracleClient.OracleClientFactory
System.Data.SqlClient.SqlClientFactory
You can obtain an instance of the appropriate DbProviderFactory
subclass using the DbProviderFactories
class, which is effectively a factory of factories. Each data provider factory is described by configuration information in the machine.config
file, similar to that shown here for the SQL Server data adapter. This can be changed or overridden by application-specific configuration information if required.
<configuration> <system.data> <DbProviderFactories> <add name="SqlClient Data Provider" invariant="System.Data.SqlClient" ~CCC description=".Net Framework Data Provider for SqlServer" type= ~CCC "System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, ~CCC Culture=neutral, PublicKeyToken=b77a5c561934e089" /> <add name="Odbc Data Provider" ... /> <add name="OleDb Data Provider" ... /> <add name="OracleClient Data Provider" ... /> <add name="SQL Server CE Data ... /> </DbProviderFactories> </system.data> </configuration>
You can enumerate the available data provider factories by calling DbProviderFactories.GetFactoryClasses
, which returns a System.Data.DataTable
containing the following columns:
Name
, which contains a human-readable name for the provider factory. Taken from the name
attribute in the configuration information.
Description
, which contains a human-readable description for the provider factory. Taken from the description
attribute of the configuration information.
InvariantName
, which contains the unique name used to refer to the data provider factory programmatically. Taken from the invariant
attribute of the configuration information.
AssemblyQualifiedName
, which contains the fully qualified name of the DbProviderFactory
class for the data provider. Taken from the type
attribute of the configuration information.
Normally, you would allow the provider to be selected at install time or the first time the application is run, and then store the settings as user or application configuration data. The most important piece of information is the InvariantName
, which you pass to the DbProviderFactories.GetFactory
method to obtain the DbProviderFactory
implementation you will use to create your IDbConnection
instances.
The following example demonstrates the enumeration of all data providers configured for the local machine and application. It then uses the DbProviderFactories
class to instantiate a DbProviderFactory
object (actually a SqlClientFactory
) from which it creates the appropriate IDbConnection
. It then uses the factory methods of the data provider interfaces to create other required objects, resulting in code that is completely generic.
using System; using System.Data; using System.Data.Common; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_10 { public static void Main(string[] args) { // Obtain the list of ADO.NET data providers registered in the // machine and application configuration files. using (DataTable providers = DbProviderFactories.GetFactoryClasses()) { // Enumerate the set of data providers and display details. Console.WriteLine("Available ADO.NET Data Providers:"); foreach (DataRow prov in providers.Rows) { Console.WriteLine(" Name:{0}", prov["Name"]); Console.WriteLine(" Description:{0}", prov["Description"]); Console.WriteLine(" Invariant Name:{0}", prov["InvariantName"]); } } // Obtain the DbProviderFactory for SQL Server. The provider to use // could be selected by the user or read from a configuration file. // In this case, we simply pass the invariant name. DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.SqlClient"); // Use the DbProviderFactory to create the initial IDbConnection, and // then the data provider interface factory methods for other objects. using (IDbConnection con = factory.CreateConnection()) { // Normally, read the connection string from secure storage. // See recipe 9-3. In this case, use a default value. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI";
// Create and configure a new command. using (IDbCommand com = con.CreateCommand()) { com.CommandType = CommandType.StoredProcedure; com.CommandText = "Ten Most Expensive Products"; // Open the connection. con.Open(); // Execute the command and process the results. using (IDataReader reader = com.ExecuteReader()) { Console.WriteLine(Environment.NewLine); Console.WriteLine("Price of the Ten Most" + " Expensive Products."); while (reader.Read()) { // Display the product details. Console.WriteLine(" {0} = {1}", reader["TenMostExpensiveProducts"], reader["UnitPrice"]); } } } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
The SqlDataSourceEnumerator
class makes it easy to enumerate the SQL Server instances accessible on the network. You simply obtain the singleton SqlDataSourceEnumerator
instance via the static property SqlDataSourceEnumerator.Instance
and call its GetDataSources
method. The GetDataSources
method returns a System.Data.DataTable
that contains a set of System.Data.DataRow
objects. Each DataRow
represents a single SQL Server instance and contains the following columns:
ServerName
, which contains the name of the server where the SQL Server instance is hosted
InstanceName
, which contains the name of the SQL Server instance or the empty string if the SQL Server is the default instance
IsClustered
, which indicates whether the SQL Server instance is part of a cluster
Version
, which contains the version of the SQL Server instance
The following example demonstrates the use of the SqlDataSourceEnumerator
class to discover and display details of all SQL Server instances accessible (and visible) on the network. The IsClustered
and Version
columns may be blank for some versions of SQL Server.
using System; using System.Data; using System.Data.Sql; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_11 { public static void Main(string[] args) { // Obtain the DataTable of SQL Server instances. using (DataTable SqlSources = SqlDataSourceEnumerator.Instance.GetDataSources()) { // Enumerate the set of SQL Servers and display details. Console.WriteLine("Discover SQL Server Instances:"); foreach (DataRow source in SqlSources.Rows) { Console.WriteLine(" Server Name:{0}", source["ServerName"]); Console.WriteLine(" Instance Name:{0}", source["InstanceName"]); Console.WriteLine(" Is Clustered:{0}", source["IsClustered"]); Console.WriteLine(" Version:{0}", source["Version"]); } }
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use System.Data.DataSet
to represent the data and System.Data.SqlClient.SqlDataAdapter
to read and sync data with the database.
The System.Data.DataSet
class contains one or more instances of System.Data.DataTable
, each of which contains instances of System.Data.DataRow
, representing data rows from the database. The SqlDataAdapter
class acts as the bridge between the database and the DataSet
, allowing you to populate the DataSet
with data and write back any changes to the database when you are done. The sequence for using a DataSet
is as follows:
Create a SqlConnection
to your database as normal (see recipe 9-1).
Create a new instance of DataSet
using the default constructor.
Create a new instance of SqlDataAdapter
, passing in a query string for the data you require and the SqlConnection
you created in step 1 as constructor arguments.
Create an instance of SqlCommandBuilder
, passing in the SqlDataAdapter
you created.
Call the SqlDataAdapter.Fill
instance method, passing the DataSet
you created in step 2 as a method argument.
Use the DataSet
to access the DataTables
contained within—read and modify data as required.
Call the SqlDataAdapter.Update
method to write any changes back to the database.
To create a new row in a table, call the DataTable.NewRow
instance method to obtain an instance of DataRow
that has the same schema as the DataTable
. The new row is not automatically added to the table when you call NewRow
—call DataTable.Rows.Add
once you have set the values for the row. Changes that you make to the data in the DataSet
are not written back to the database until you call the SqpDataAdapter.Update
method.
The following example creates a DataSet
and fills it with the contents of the Region
table of the Northwind
sample database. The DataSet
contains one DataTable
, whose schema and contents are printed out. A new record is added and an existing one modified before the changes are written back to the database.
using System; using System.Data; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_12 { static void Main(string[] args) { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Open the database connection. con.Open(); // Create the query string. string query = "SELECT * from Region"; // Create the data set. DataSet dataset = new DataSet(); // Create the SQL data adapter. SqlDataAdapter adapter = new SqlDataAdapter(query, con); // Create the command builder so we can do modifications. SqlCommandBuilder commbuilder = new SqlCommandBuilder(adapter); // Populate the data set from the database. adapter.Fill(dataset);
// Print details of the schema. Console.WriteLine(" Schema for table"); DataTable table = dataset.Tables[0]; foreach (DataColumn col in table.Columns) { Console.WriteLine("Column: {0} Type: {1}", col.ColumnName, col.DataType); } // Enumerate the data we have received. Console.WriteLine(" Data in table"); foreach (DataRow row in table.Rows) { Console.WriteLine("Data {0} {1}", row[0], row[1]); } // Create a new row. DataRow newrow = table.NewRow(); newrow["RegionID"] = 5; newrow["RegionDescription"] = "Central"; table.Rows.Add(newrow); // Modify an existing row. table.Rows[0]["RegionDescription"] = "North Eastern"; // Enumerate the cached data again. // Enumerate the data we have received. Console.WriteLine(" Data in (modified) table"); foreach (DataRow row in table.Rows) { Console.WriteLine("Data {0} {1}", row[0], row[1]); } // Write the data back to the database. adapter.Update(dataset); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Running the example produces the following results:
Schema for table Column: RegionID Type: System.Int32 Column: RegionDescription Type: System.String Data in table Data 1 Eastern Data 2 Western Data 3 Northern Data 4 Southern Data in (modified) table Data 1 North Eastern Data 2 Western Data 3 Northern Data 4 Southern Data 5 Central Main method complete. Press Enter.
Create an instance of System.Sql.DataSet
and manually populate it with instances of System.Data.Datatable
. Create a schema for each table and create rows to represent data elements.
In the previous recipe, we demonstrated how to use the DataSet
and DataTable
classes as part of a memory cache, in order to achieve disconnected data manipulation. However, you can create instances of these classes to represent data programmatically by calling constructors for the classes directly. The example code for this recipe illustrates how to do this in order to create the same kind of DataSet
and DataTable
that we used previously.
The following code creates a DataSet
that contains a single DataTable
and populates it with instances of DataRow
. Once populated, the same queries, modifications, and additions are performed upon it as in the previous recipe.
using System; sing System.Data; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_13 { static void Main(string[] args) { // Create the data set. DataSet dataset = new DataSet(); // Create the table and add it to the data set. DataTable table = new DataTable("Regions"); dataset.Tables.Add(table); // Create the colums for the table. table.Columns.Add("RegionID", typeof(int)); table.Columns.Add("RegionDescription", typeof(string));
// Populate the table. string[] regions = { "Eastern", "Western", "Northern", "Southern" }; for (int i = 0; i < regions.Length; i++) { DataRow row = table.NewRow(); row["RegionID"] = i + 1; row["RegionDescription"] = regions[i]; table.Rows.Add(row); } // Print details of the schema. Console.WriteLine(" Schema for table"); foreach (DataColumn col in table.Columns) { Console.WriteLine("Column: {0} Type: {1}", col.ColumnName, col.DataType); } // Enumerate the data we have received. Console.WriteLine(" Data in table"); foreach (DataRow row in table.Rows) { Console.WriteLine("Data {0} {1}", row[0], row[1]); } // Create a new row. DataRow newrow = table.NewRow(); newrow["RegionID"] = 5; newrow["RegionDescription"] = "Central"; table.Rows.Add(newrow); // Modify an existing row. table.Rows[0]["RegionDescription"] = "North Eastern"; // Enumerate the cached data again. // Enumerate the data we have received. Console.WriteLine(" Data in (modified) table"); foreach (DataRow row in table.Rows) { Console.WriteLine("Data {0} {1}", row[0], row[1]); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
The program produces the following output:
Schema for table Column: RegionID Type: System.Int32 Column: RegionDescription Type: System.String Data in table Data 1 Eastern Data 2 Western Data 3 Northern Data 4 Southern Data in (modified) table Data 1 North Eastern Data 2 Western Data 3 Northern Data 4 Southern Data 5 Central Main method complete. Press Enter.
Create or obtain an instance of DataTable
(see recipes 9-12 and 9-13) and call the AsEnumerable
instance method to obtain an IEnumerable<DataRow>
, which can be used as a data source for LINQ queries.
LINQ performs queries on the IEnumerable<>
type, which you can obtain from instances of DataTable
using the AsEnumerable
instance method. When using SQLDataAdapter
to populate instances of DataTable
with data (see recipe 9-12), remember that you are working with cached data that will not reflect changes made to the database. See Chapter 16 for recipes that demonstrate LINQ features.
The following example creates a DataSet
that contains a DataTable
with all of the rows of the Northwind Region
table, and then performs a LINQ query using the DataTable
as the data source:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_14 { static void Main(string[] args) { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Open the database connection. con.Open(); // Create the query string. string query = "SELECT * from Region"; // Create the data set. DataSet dataset = new DataSet();
// Create the SQL data adapter. SqlDataAdapter adapter = new SqlDataAdapter(query, con); // Create the command builder so we can do modifications. SqlCommandBuilder commbuilder = new SqlCommandBuilder(adapter); // Populate the data set from the database. adapter.Fill(dataset); // Obtain the data table. DataTable table = dataset.Tables[0]; // Perform the LINQ query. IEnumerable<string> result = from e in table.AsEnumerable() where e.Field<int>(0) < 3 select e.Field<string>(1); // Enumerate the results of the LINQ query. foreach (string str in result) { Console.WriteLine("Result: {0}", str); } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Running the program gives the following results:
Result: Eastern Result: Western Main method complete. Press Enter.
Define and annotate types with the Table
and Column
annotations and use System.Data.Linq.DataContext
to access the data in your database.
LINQ includes support for entity classes, which map the schema from your database into .NET types. You create an entity type by defining a partial class with members representing columns in a given database table and apply annotations from the System.Data.Linq.Mapping
namespace to give the .NET Framework details of how to map from the table to instances of your entity type.
Visual Studio can automatically create types for you. Select the Add a New Item option for your project, and then select LINQ to SQL Classes to start a wizard that will generate the source files you require.
The first annotation to apply is Table
, which creates the relationship between the partial class you have defined and the table in the database—this annotation takes one argument, which is, not surprisingly, the name of the table in question. You must then define one member for each column in the table (ensuring that the member type matches the schema type for the database table), and apply the Column
annotation. For the Region
table in the Northwind
database, we would create a class like this:
[Table(Name = "Region")] public partial class Region { [Column] public int RegionID; [Column] public string RegionDescription; }
To use the entity type, create an instance of System.Data.Linq.DataContext
, passing in a SqlConnection
to your database as the constructor argument. You then call the DataContext.GetTable<>
instance method using your entity class as the type annotation—for example:
Table<Region> regionstable = context.GetTable<Region>();
The result from the GetTable
method is a strongly typed instance of System.Data.Linq.Table
, which you can use as the data source for a LINQ query. In the clauses of the query, you can refer to the members of your entity type to perform filters and select results—see the code for this recipe for a demonstration.
LINQ entity types have a lot of features beyond what we have demonstrated here—see the .NET documentation for further details. The LINQ to SQL home page is a good starting point: http://msdn.microsoft.com/en-us/library/bb386976(VS.100).aspx
.
The following example defines the type Region
to represent rows in the Northwind Region
table. A DataContext
is created to access the data, and the Region
table is used as the basis for a LINQ query, returning an IEnumeration<Region>
as the result.
You must add the System.Data.Linq.dll
assembly to your project in order to use the System.Data.Linq
and System.Data.Linq.Mapping
namespaces.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; using System.Data.Linq; using System.Data.Linq.Mapping; namespace Apress.VisualCSharpRecipes.Chapter09 { [Table(Name = "Region")] public partial class Region { [Column] public int RegionID; [Column] public string RegionDescription; } class Recipe09_15 { static void Main(string[] args) {
// Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) { // Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Open the database connection. con.Open(); // Create the data context. DataContext context = new DataContext(con); // Get the table we are interested in. Table<Region> regionstable = context.GetTable<Region>(); IEnumerable<Region> result = from e in regionstable where e.RegionID < 3 select e; foreach (Region res in result) { Console.WriteLine("RegionID {0} Descr: {1}", res.RegionID, res.RegionDescription); } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use the Union, Intersect
, or Except
extension method to compare the results of two LINQ queries.
The default result of a LINQ query on a DataSet
is an IEnumerable<DataRow>
, and LINQ provides extension methods that operate on this result type to allow you to compare results.
See Chapter 16 for more information about LINQ extension methods, recipes for using them, and creating custom extension methods that you can apply to your own data types.
The three extension methods are Union, Intersect
, and Except
. In all three cases, you call the extension method on one result and supply another as the method argument—for example:
IEnumerable<DataRow> result1 = ...LINQ query on a DataSet... IEnumerable<DataRow> result2 = ...LINQ query on a DataSet... IEnumerable<DataRow> union = result1.Union(result2)
The Union
method combines the contents of the two IEnumerable<DataRow>
instances. The Intersect
method returns just those rows that exist in both enumerations. The Except
method returns all of the rows in the first enumeration except those that also exist in the second enumeration.
The result of these methods is another IEnumerable<DataRow>
, meaning that you can use the result to enumerate the data rows or as the basis for a further LINQ query, and you can use the same extension methods to compare the result against another IEnumerable<DataRow>
.
The following program performs two queries against the same table and then uses the Union, Intersect
, and Except
methods to compare the results:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace Apress.VisualCSharpRecipes.Chapter09 { class Recipe09_16 { static void Main(string[] args) { // Create a new SqlConnection object. using (SqlConnection con = new SqlConnection()) {
// Configure the SqlConnection object's connection string. con.ConnectionString = @"Data Source = .sqlexpress;" + "Database = Northwind; Integrated Security=SSPI"; // Open the database connection. con.Open(); // Create the query string. string query = "SELECT * from Region"; // Create the data set. DataSet dataset = new DataSet(); // Create the SQL data adapter. SqlDataAdapter adapter = new SqlDataAdapter(query, con); // Create the command builder so we can do modifications. SqlCommandBuilder commbuilder = new SqlCommandBuilder(adapter); // Populate the data set from the database. adapter.Fill(dataset); // Obtain the data table. DataTable table = dataset.Tables[0]; // Perform the first LINQ query. IEnumerable<DataRow> result1 = from e in table.AsEnumerable() where e.Field<int>(0) < 3 select e; // Enumerate the results of the first LINQ query. Console.WriteLine("Results from first LINQ query"); foreach (DataRow row in result1) { Console.WriteLine("ID: {0} Name: {1}", row.Field<int>(0), row.Field<string>(1)); } // Perform the first LINQ query. IEnumerable<DataRow> result2 = from e in table.AsEnumerable() let name = e.Field<string>(1) where name.StartsWith("North") || name.StartsWith("East") select e;
// Enumerate the results of the first LINQ query. Console.WriteLine(" Results from second LINQ query"); foreach (DataRow row in result2) { Console.WriteLine("ID: {0} Name: {1}", row.Field<int>(0), row.Field<string>(1)); } IEnumerable<DataRow> union = result1.Union(result2); // Enumerate the results. Console.WriteLine(" Results from union"); foreach (DataRow row in union) { Console.WriteLine("ID: {0} Name: {1}", row.Field<int>(0), row.Field<string>(1)); } IEnumerable<DataRow> intersect = result1.Intersect(result2); // Enumerate the results. Console.WriteLine(" Results from intersect"); foreach (DataRow row in intersect) { Console.WriteLine("ID: {0} Name: {1}", row.Field<int>(0), row.Field<string>(1)); } IEnumerable<DataRow> except = result1.Except(result2); // Enumerate the results. Console.WriteLine(" Results from except"); foreach (DataRow row in except) { Console.WriteLine("ID: {0} Name: {1}", row.Field<int>(0), row.Field<string>(1)); } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Running the sample program gives the following results:
Results from first LINQ query ID: 1 Name: Eastern ID: 2 Name: Western Results from second LINQ query ID: 1 Name: Eastern ID: 3 Name: Northern Results from union ID: 1 Name: Eastern ID: 2 Name: Western ID: 3 Name: Northern Results from intersect ID: 1 Name: Eastern Results from except ID: 2 Name: Western Main method complete. Press Enter.
3.22.234.198