Disconnected Data in ADO.NET
ADO.NET introduces the concept of disconnected data, which is a fundamental pattern for accessing and manipulating data in modern applications. Unlike connected data scenarios where a constant connection to the data source is maintained, disconnected data allows you to fetch data, close the connection, and then work with that data locally in memory.
This approach offers several advantages:
- Reduced Network Traffic: Data is retrieved in batches, minimizing the number of round trips to the database.
- Improved Scalability: Applications can handle more users because database connections are not held open for extended periods.
- Enhanced Responsiveness: User interfaces remain responsive as data operations don't block the main application thread.
- Offline Scenarios: Applications can function even when the data source is temporarily unavailable.
Key Components for Disconnected Data
The primary classes in ADO.NET that facilitate disconnected data access are:
DataSet
A DataSet
is an in-memory representation of data. It can hold multiple tables (DataTable
objects), each with its own rows (DataRow
objects) and columns (DataColumn
objects). A DataSet
can also contain relationships between these tables, mimicking the structure of a relational database.
DataSet
objects are rich in features, including data validation, constraints, and the ability to track changes to rows (added, modified, or deleted).
DataTable
A DataTable
represents a single table of data in memory. It contains a collection of DataRow
objects and defines the schema (columns and their types) through a collection of DataColumn
objects.
DataRow
An individual record within a DataTable
. You can access and modify the values of each column in a DataRow
.
DataAdapter
The DataAdapter
acts as a bridge between a DataSet
and a data source. It's responsible for filling a DataSet
with data from the source and for synchronizing changes made in the DataSet
back to the data source. Key DataAdapter
classes include:
SqlDataAdapter
(for SQL Server)OleDbDataAdapter
(for OLE DB providers)OracleDataAdapter
(for Oracle)
Working with Disconnected Data
The typical workflow for disconnected data involves the following steps:
- Create a
DataSet
: Instantiate a newDataSet
object. - Create a
DataAdapter
: Instantiate the appropriateDataAdapter
for your data source. - Configure the
DataAdapter
: Set itsSelectCommand
to define how to retrieve data and itsInsertCommand
,UpdateCommand
, andDeleteCommand
to define how to write changes back. - Fill the
DataSet
: Use theFill()
method of theDataAdapter
to populate theDataSet
with data. This automatically opens and closes the connection. - Work with Data Locally: Perform operations on the
DataSet
(e.g., filtering, sorting, editing rows) without maintaining an active connection to the database. - Update the Data Source: When ready, use the
Update()
method of theDataAdapter
to send any changes (new rows, modified rows, deleted rows) back to the database. This method intelligently generates the appropriate SQL commands based on the row state.
Example: Retrieving and Updating Data
Here's a simplified C# example demonstrating the disconnected data pattern:
using System;
using System.Data;
using System.Data.SqlClient;
public class DisconnectedDataExample
{
public static void Main()
{
string connectionString = "Your_Connection_String_Here";
string query = "SELECT ProductID, ProductName, UnitPrice FROM Products WHERE Discontinued = 0";
// 1. Create DataSet and DataAdapter
DataSet productsDataSet = new DataSet("ProductsCatalog");
using (SqlConnection connection = new SqlConnection(connectionString))
{
SqlDataAdapter adapter = new SqlDataAdapter(query, connection);
// Define Update Commands (simplified for demonstration)
// In a real app, these would be more robust
adapter.UpdateCommand = new SqlCommand("UPDATE Products SET ProductName = @ProductName, UnitPrice = @UnitPrice WHERE ProductID = @ProductID", connection);
adapter.UpdateCommand.Parameters.Add("@ProductName", SqlDbType.VarChar, 100, "ProductName");
adapter.UpdateCommand.Parameters.Add("@UnitPrice", SqlDbType.Money, 0, "UnitPrice");
var parameter = adapter.UpdateCommand.Parameters.Add("@ProductID", SqlDbType.Int, 4, "ProductID");
parameter.SourceVersion = DataRowVersion.Original;
adapter.InsertCommand = new SqlCommand("INSERT INTO Products (ProductName, UnitPrice) VALUES (@ProductName, @UnitPrice)", connection);
adapter.InsertCommand.Parameters.Add("@ProductName", SqlDbType.VarChar, 100, "ProductName");
adapter.InsertCommand.Parameters.Add("@UnitPrice", SqlDbType.Money, 0, "UnitPrice");
adapter.DeleteCommand = new SqlCommand("DELETE FROM Products WHERE ProductID = @ProductID", connection);
parameter = adapter.DeleteCommand.Parameters.Add("@ProductID", SqlDbType.Int, 4, "ProductID");
parameter.SourceVersion = DataRowVersion.Original;
// 2. Fill the DataSet
adapter.Fill(productsDataSet, "Products");
}
// --- Disconnected Operations ---
DataTable productsTable = productsDataSet.Tables["Products"];
// 3. Modify Data
Console.WriteLine("--- Original Data ---");
foreach (DataRow row in productsTable.Rows)
{
Console.WriteLine($"{row["ProductName"]} - {row["UnitPrice"]}");
if (row["ProductName"].ToString() == "Chai")
{
row["UnitPrice"] = 20.50m;
row.SetModified(); // Mark as modified
}
}
// Add a new row
DataRow newRow = productsTable.NewRow();
newRow["ProductName"] = "New Gadget";
newRow["UnitPrice"] = 99.99m;
productsTable.Rows.Add(newRow);
// newRow.SetAdded(); // This is automatically done by Add for new rows
// 4. Update the Data Source
using (SqlConnection connection = new SqlConnection(connectionString))
{
SqlDataAdapter adapter = new SqlDataAdapter(query, connection);
// Re-configure commands for updating
adapter.UpdateCommand = new SqlCommand("UPDATE Products SET ProductName = @ProductName, UnitPrice = @UnitPrice WHERE ProductID = @ProductID", connection);
adapter.UpdateCommand.Parameters.Add("@ProductName", SqlDbType.VarChar, 100, "ProductName");
adapter.UpdateCommand.Parameters.Add("@UnitPrice", SqlDbType.Money, 0, "UnitPrice");
parameter = adapter.UpdateCommand.Parameters.Add("@ProductID", SqlDbType.Int, 4, "ProductID");
parameter.SourceVersion = DataRowVersion.Original;
adapter.InsertCommand = new SqlCommand("INSERT INTO Products (ProductName, UnitPrice) VALUES (@ProductName, @UnitPrice)", connection);
adapter.InsertCommand.Parameters.Add("@ProductName", SqlDbType.VarChar, 100, "ProductName");
adapter.InsertCommand.Parameters.Add("@UnitPrice", SqlDbType.Money, 0, "UnitPrice");
adapter.DeleteCommand = new SqlCommand("DELETE FROM Products WHERE ProductID = @ProductID", connection);
parameter = adapter.DeleteCommand.Parameters.Add("@ProductID", SqlDbType.Int, 4, "ProductID");
parameter.SourceVersion = DataRowVersion.Original;
int rowsAffected = adapter.Update(productsDataSet, "Products");
Console.WriteLine($"\n{rowsAffected} rows updated successfully.");
}
}
}
This pattern is highly effective for building robust and scalable .NET applications that interact with data.
For more detailed information, refer to the official ADO.NET documentation and explore concepts like change tracking and concurrency handling.