Chapter 9. Database Access

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

.NET Framework Data Provider for ODBC

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 System.Data.Odbc namespace and have the prefix Odbc.

.NET Framework Data Provider for OLE DB

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 System.Data.OleDb namespace and have the prefix OleDb.

.NET Framework Data Provider for Oracle

Provides optimized connectivity to Oracle databases via Oracle client software version 8.1.7 or later. Data provider classes are contained in the System.Data.OracleClient namespace and have the prefix Oracle.

.NET Framework Data Provider for SQL Server

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 System.Data.SqlClient namespace and have the prefix Sql.

.NET Compact Framework Data Provider

Provides connectivity to Microsoft SQL Server CE. Data provider classes are contained in the System.Data.SqlServerCe namespace and have the prefix SqlCe.

Tip

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)

Note

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.

Connect to a Database

Problem

You need to open a connection to a database.

Solution

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.

How It Works

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 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();
        }
    }
}

Use Connection Pooling

Problem

You need to use a pool of database connections to improve application performance and scalability.

Solution

Configure the connection pool using settings in the connection string of a connection object.

How It Works

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.

Note

Once created, a pool exists until your process terminates.

Table 9.2. Connection String Settings That Control Connection Pooling

Setting

Description

Connection Lifetime

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 0, which means connections exist for the life of the current process.

Connection Reset

Supported only by the SQL Server data provider. Specifies whether connections are reset as they are taken from the pool. A value of True (the default) ensures that a connection's state is reset, but requires additional communication with the database.

Max Pool Size

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 100.

Min Pool Size

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 0.

Pooling

Set to False to obtain a nonpooled connection. The default value is True.

The Code

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();
        }
    }
}

Notes

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.

Create a Database Connection String Programmatically

Problem

You need to programmatically create or modify a syntactically correct connection string by working with its component parts or parsing a given connection string.

Solution

Use the System.Data.Common.DbConnectionStringBuilder class or one of its strongly typed subclasses that form part of an ADO.NET data provider.

How It Works

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 Code

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 a Database Connection String Securely

Problem

You need to store a database connection string securely.

Solution

Store the connection string in an encrypted section of the application's configuration file.

Note

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.

How It Works

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.

Note

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 Code

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.

Note

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();
        }
    }
}

Execute a SQL Command or Stored Procedure

Problem

You need to execute a SQL command or stored procedure on a database.

Solution

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.

How It Works

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

CommandText

A string containing the text of the SQL command to execute or the name of a stored procedure. The content of the CommandText property must be compatible with the value you specify in the CommandType property.

CommandTimeout

An int that specifies the number of seconds to wait for the command to return before timing out and raising an exception. Defaults to 30 seconds.

CommandType

A value of the System.Data.CommandType enumeration that specifies the type of command represented by the command object. For most data providers, valid values are StoredProcedure, when you want to execute a stored procedure; and Text, when you want to execute a SQL text command. If you are using the OLE DB data provider, you can specify TableDirect when you want to return the entire contents of one or more tables; refer to the .NET Framework SDK documentation for more details. Defaults to Text.

Connection

An IDbConnection instance that provides the connection to the database on which you will execute the command. If you create the command using the IDbConnection.CreateCommand method, this property will be automatically set to the IDbConnection instance from which you created the command.

Parameters

A System.Data.IDataParameterCollection instance containing the set of parameters to substitute into the command. (See recipe 9-6 for details on how to use parameters.)

Transaction

A System.Data.IDbTransaction instance representing the transaction into which to enlist the command. (See the .NET Framework SDK documentation for details about transactions.)

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.

Note

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 Code

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();
        }
    }
}

Use Parameters in a SQL Command or Stored Procedure

Problem

You need to set the arguments of a stored procedure or use parameters in a SQL command to improve flexibility.

Solution

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.

How It Works

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

DbType

A value of the System.Data.DbType enumeration that specifies the type of data contained in the parameter. Commonly used values include String, Int32, DateTime, and Currency.

Direction

A value from the System.Data.ParameterDirection enumeration that indicates the direction in which the parameter is used to pass data. Valid values are Input, InputOutput, Output, and ReturnValue.

IsNullable

A bool that indicates whether the parameter accepts null values.

ParameterName

A string containing the name of the parameter.

Value

An object containing the value of the parameter.

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 Code

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();
        }
    }
}

Process the Results of a SQL Query Using a Data Reader

Problem

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).

Solution

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.

How It Works

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

 

FieldCount

Gets the number of columns in the current row.

IsClosed

Returns true if IDataReader is closed, and false if it's currently open.

Item

Returns an object representing the value of the specified column in the current row. Columns can be specified using a zero-based integer index or a string containing the column name. You must cast the returned value to the appropriate type. This is the indexer for data record and reader classes.

Method

 

GetDataTypeName

Gets the name of the data source data type for a specified column.

GetFieldType

Gets a System.Type instance representing the data type of the value contained in the column specified using a zero-based integer index.

GetName

Gets the name of the column specified by using a zero-based integer index.

GetOrdinal

Gets the zero-based column ordinal for the column with the specified name.

GetSchemaTable

Returns a System.Data.DataTable instance that contains metadata describing the columns contained in IDataReader.

IsDBNull

Returns true if the value in the specified column contains a data source null value; otherwise, it returns false.

NextResult

Moves to the next set of results if IDataReader includes multiple result sets because multiple statements were executed. By default, IDataReader is positioned on the first result set.

Read

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 Code

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();
        }
    }
}

Obtain an XML Document from a SQL Server Query

Problem

You need to execute a query against a SQL Server database and retrieve the results as XML.

Solution

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.

How It Works

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>

Tip

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 Code

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();
        }
    }
}

Perform Asynchronous Database Operations Against SQL Server

Problem

You need to execute a query or command against a SQL Server database as a background task while your application continues with other processing.

Solution

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.

Note

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.

How It Works

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.

Note

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.

Warning

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.

The Code

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();
        }
    }
}

Write Database-Independent Code

Problem

You need to write code that can be configured to work against any relational database supported by an ADO.NET data provider.

Solution

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.

How It Works

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

IDbConnection

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 DbProviderFactory.CreateConnection factory method (discussed in this recipe).

Recipe 9-1

IDbCommand

Represents a SQL command that is issued to a relational database. You can create IDbCommand objects of the appropriate type using the IDbConnection.CreateCommand or DbProviderFactory.CreateCommand factory method.

Recipe 9-5

IDataParameter

Represents a parameter to an IDbCommand object. You can create IDataParameter objects of the correct type using the IDbCommand.CreateParameter, IDbCommand.Parameters.Add, or DbProviderFactory.CreateParameter factory method.

Recipe 9-6

IDataReader

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 IDbCommand.ExecuteReader method.

Recipes 9-5 and 9-6

IDbDataAdapter

Represents the set of commands used to fill a System.Data.DataSet from a relational database and to update the database based on changes to the DataSet. You must program the logic to create a data adapter object of the appropriate type based on your application's configuration information, or use the DbProviderFactory.CreateAdapter factory method (discussed in this recipe).

 

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 Code

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();
        }
    }
}

Discover All Instances of SQL Server on Your Network

Problem

You need to obtain a list of all instances of SQL Server that are accessible on the network.

Solution

Use the GetDataSources method of the System.Data.Sql.SqlDataSourceEnumerator class.

How It Works

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 Code

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();
        }
    }
}

Create an In-Memory Cache

Problem

You need to create an in-memory cache of part of the database.

Solution

Use System.Data.DataSet to represent the data and System.Data.SqlClient.SqlDataAdapter to read and sync data with the database.

How It Works

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:

  1. Create a SqlConnection to your database as normal (see recipe 9-1).

  2. Create a new instance of DataSet using the default constructor.

  3. 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.

  4. Create an instance of SqlCommandBuilder, passing in the SqlDataAdapter you created.

  5. Call the SqlDataAdapter.Fill instance method, passing the DataSet you created in step 2 as a method argument.

  6. Use the DataSet to access the DataTables contained within—read and modify data as required.

  7. 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 Code

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 a DataSet Programmatically

Problem

You need to work with in-memory data without a database.

Solution

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.

How It Works

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 Code

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.

Perform a LINQ Query

Problem

You need to use LINQ to query a database.

Solution

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.

How It Works

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 Code

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.

Perform a LINQ Query with Entity Types

Problem

You need to work with types when using LINQ.

Solution

Define and annotate types with the Table and Column annotations and use System.Data.Linq.DataContext to access the data in your database.

How It Works

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.

Tip

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.

Tip

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 Code

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.

Note

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();
        }
    }
}

Compare LINQ DataSet Results

Problem

You need to compare the results of a LINQ query.

Solution

Use the Union, Intersect, or Except extension method to compare the results of two LINQ queries.

How It Works

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.

Tip

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 Code

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.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.234.198