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:

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:

Working with Disconnected Data

The typical workflow for disconnected data involves the following steps:

  1. Create a DataSet: Instantiate a new DataSet object.
  2. Create a DataAdapter: Instantiate the appropriate DataAdapter for your data source.
  3. Configure the DataAdapter: Set its SelectCommand to define how to retrieve data and its InsertCommand, UpdateCommand, and DeleteCommand to define how to write changes back.
  4. Fill the DataSet: Use the Fill() method of the DataAdapter to populate the DataSet with data. This automatically opens and closes the connection.
  5. Work with Data Locally: Perform operations on the DataSet (e.g., filtering, sorting, editing rows) without maintaining an active connection to the database.
  6. Update the Data Source: When ready, use the Update() method of the DataAdapter 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.