Skip to main content

Async DataTable

The AdoNet.Async.DataSet package provides async wrappers for DataTable, DataRow, and DataRowCollection. These wrappers ensure that all mutations go through async methods, which in turn fire async events.

AsyncDataTable

AsyncDataTable wraps a System.Data.DataTable. You can create one from scratch or wrap an existing DataTable.

using System.Data.Async.DataSet;

// Create from scratch
var table = new AsyncDataTable("Users");
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Columns.Add("Email", typeof(string));

// Or wrap an existing DataTable
DataTable existing = GetDataTableFromSomewhere();
var wrapped = new AsyncDataTable(existing);

Properties forwarded to the inner DataTable

Most properties are forwarded directly to the inner DataTable:

PropertyTypeDescription
TableNamestringGet/set the table name
NamespacestringGet/set the XML namespace
PrefixstringGet/set the XML prefix
CaseSensitiveboolGet/set case sensitivity for string comparisons
LocaleCultureInfoGet/set the locale for string comparisons
DisplayExpressionstringGet/set the expression used for display
HasErrorsboolWhether any row has errors
MinimumCapacityintGet/set the initial capacity
IsInitializedboolWhether the table has been initialized
PrimaryKeyDataColumn[]Get/set the primary key columns

Collections

PropertyTypeNotes
ColumnsDataColumnCollectionThe standard DataColumnCollection -- column definitions are synchronous
RowsAsyncDataRowCollectionAsync wrapper with AddAsync, RemoveAsync, RemoveAtAsync
ConstraintsConstraintCollectionStandard constraint collection
ParentRelationsDataRelationCollectionStandard parent relation collection
ChildRelationsDataRelationCollectionStandard child relation collection
DefaultViewDataViewThe default view
ExtendedPropertiesPropertyCollectionCustom properties

Creating rows

// Create a detached row (not yet in the table)
AsyncDataRow row = table.NewRow();

// Set values asynchronously
await row.SetValueAsync("Id", 1);
await row.SetValueAsync("Name", "Alice");
await row.SetValueAsync("Email", "alice@example.com");

// Add to the table
await table.Rows.AddAsync(row);

Table-level async operations

// Clear all rows (fires TableClearingAsync, then TableClearedAsync)
await table.ClearAsync();

// Accept changes on all rows (fires RowChangedAsync for each modified/added row)
await table.AcceptChangesAsync();
warning

AcceptChanges() and Clear() are marked [Obsolete] with error: true on AsyncDataTable. This is deliberate -- calling them would bypass async events. Use AcceptChangesAsync() and ClearAsync() instead.

Other methods

AsyncDataTable also exposes forwarded methods for common operations:

  • Clone() / Copy() -- Returns a new AsyncDataTable (schema only / schema + data)
  • Merge(AsyncDataTable) -- Merges rows from another table
  • Select() / Select(filter) / Select(filter, sort) -- Returns AsyncDataRow[] matching the filter
  • GetChanges() / GetChanges(DataRowState) -- Returns a new AsyncDataTable with only changed rows
  • GetErrors() -- Returns AsyncDataRow[] for rows with errors
  • ImportRow(AsyncDataRow) -- Imports a row from another table
  • LoadDataRow(values, fAcceptChanges) -- Loads values into a matching row or creates a new one
  • Compute(expression, filter) -- Computes an aggregate expression
  • LoadAsync(IAsyncDataReader) -- Asynchronously loads rows from a reader

AsyncDataRow

AsyncDataRow wraps a System.Data.DataRow. Its indexers are read-only -- all writes go through SetValueAsync, which fires async events.

Read-only indexers

// Access by column name
object value = row["Name"];

// Access by column index
object value = row[0];

// Access by DataColumn
object value = row[table.Columns["Name"]!];

// Access a specific version
object original = row["Name", DataRowVersion.Original];
object current = row["Name", DataRowVersion.Current];

Async mutations

// Set by column name
await row.SetValueAsync("Name", "Bob");

// Set by column index
await row.SetValueAsync(0, 42);

// Set by DataColumn
await row.SetValueAsync(table.Columns["Name"]!, "Charlie");

// Delete the row (marks as Deleted)
await row.DeleteAsync();

// Edit lifecycle
await row.BeginEditAsync();
// ... make changes via SetValueAsync ...
await row.EndEditAsync(); // fires RowChangingAsync + RowChangedAsync
await row.CancelEditAsync(); // discards pending edits

// Accept or reject changes on this row
await row.AcceptChangesAsync();
await row.RejectChangesAsync();

Row state and properties

PropertyTypeDescription
RowStateDataRowStateDetached, Unchanged, Added, Modified, Deleted
HasErrorsboolWhether the row has errors
RowErrorstringThe error description
TableAsyncDataTableThe parent table
HasVersion(DataRowVersion)boolWhether the row has a specific version

Event flow for SetValueAsync

Each call to SetValueAsync fires events in this order:

  1. ColumnChangingAsync -- before the value is set
  2. The value is written to the inner DataRow
  3. ColumnChangedAsync -- after the value is set

See Async Events for the complete event firing order for all mutations.

AsyncDataRowCollection

AsyncDataRowCollection wraps DataRowCollection and provides async add/remove operations.

Adding rows

// Add a prepared row
AsyncDataRow row = table.NewRow();
await row.SetValueAsync("Name", "Alice");
await table.Rows.AddAsync(row);

// Add from an array of values
await table.Rows.AddAsync([1, "Alice", "alice@example.com"]);

Removing rows

// Remove a specific row
await table.Rows.RemoveAsync(row);

// Remove by index
await table.Rows.RemoveAtAsync(0);

Enumeration

AsyncDataRowCollection implements IEnumerable<AsyncDataRow>, so you can iterate with a standard foreach:

foreach (AsyncDataRow row in table.Rows)
{
Console.WriteLine(row["Name"]);
}

Indexing

int count = table.Rows.Count;
AsyncDataRow first = table.Rows[0];
bool hasKey = table.Rows.Contains(42);

Accessing the inner DataTable

Sometimes you need access to the underlying DataTable -- for example, to pass it to a library that expects System.Data types. Use an explicit cast:

AsyncDataTable asyncTable = new AsyncDataTable("Users");
// ... populate ...

// Get the inner DataTable via explicit cast
DataTable inner = (DataTable)asyncTable;
warning

Mutations made directly on the inner DataTable bypass async events. Only use the explicit cast when you need interop with code that requires a DataTable, and avoid mutating through the inner reference.

Identity semantics

AsyncDataTable maintains a ConditionalWeakTable<DataRow, AsyncDataRow> cache. This means that the same underlying DataRow always returns the same AsyncDataRow wrapper instance:

AsyncDataRow row1 = table.Rows[0];
AsyncDataRow row2 = table.Rows[0];
bool same = ReferenceEquals(row1, row2); // true

This is important for event handlers that compare row references, and for any code that uses row identity rather than row equality.

The cache uses weak references, so AsyncDataRow wrappers are eligible for garbage collection when no longer referenced -- there is no memory leak from the caching mechanism.

Complete example

using System.Data.Async.DataSet;

// Create and configure a table
var table = new AsyncDataTable("Employees");
table.Columns.Add("Id", typeof(int));
table.Columns.Add("Name", typeof(string));
table.Columns.Add("Department", typeof(string));
table.Columns.Add("Salary", typeof(decimal));
table.PrimaryKey = [table.Columns["Id"]!];

// Subscribe to an async event
table.RowChangedAsync += async (args, ct) =>
{
Console.WriteLine($"Row changed: {args.Action}");
await Task.CompletedTask;
};

// Add rows
var row = table.NewRow();
await row.SetValueAsync("Id", 1);
await row.SetValueAsync("Name", "Alice");
await row.SetValueAsync("Department", "Engineering");
await row.SetValueAsync("Salary", 95000m);
await table.Rows.AddAsync(row);

await table.Rows.AddAsync([2, "Bob", "Marketing", 85000m]);
await table.Rows.AddAsync([3, "Charlie", "Engineering", 105000m]);

// Query
AsyncDataRow[] engineers = table.Select("Department = 'Engineering'");
Console.WriteLine($"Engineers: {engineers.Length}"); // 2

// Modify
await table.Rows[1].SetValueAsync("Salary", 90000m);

// Check changes
AsyncDataTable? changes = table.GetChanges(DataRowState.Modified);
Console.WriteLine($"Modified rows: {changes?.Rows.Count}"); // 1

// Accept all changes
await table.AcceptChangesAsync();

// Delete and clear
await table.Rows[0].DeleteAsync();
await table.ClearAsync();