The foundation
Most of the library is a provider-agnostic set of extension methods on the standard ADO.NET types (IDbConnection, DbConnection, IDbCommand, IDataReader, IDbTransaction). They apply common best practices so that everyday database work is short to write and safe by default. Some helpers are deliberately thin and unopinionated; some are deliberately opinionated, for example a command built from a factory always disposes the connection it created.
The expressive commands and the row mapping are built on this layer.
Connection factories
A connection factory creates connections without the caller knowing a connection string:
IDbConnectionFactoryandIDbConnectionFactory<TConnection>are the abstractions, which fit dependency injection.DbConnectionFactory<TConnection>wraps anyFunc<TConnection>.SqlConnectionFactory(in theMSSqlClientpackage) wraps a connection string for SQL Server.
// A connection factory: each command built from it opens its own connection and disposes it afterwards.
// Held in a lazily created field (the C# 14 `field` keyword). The factory is stateless, so a benign race costs nothing.
public static IDbConnectionFactory<SqliteConnection> Factory
{
get => field ??= new DbConnectionFactory<SqliteConnection>(() => new SqliteConnection(ConnectionString));
}
Helpers on a factory: Using creates a connection, runs your action, and disposes the connection (it does not open it). Open does the same after making sure the connection is open. Command and StoredProcedure start an expressive command.
Who owns the connection
| You start from | The connection is... | When a terminal runs |
|---|---|---|
A connection factory (factory.Command(sql)) |
Created by the command | Created, opened if needed, then disposed |
A connection (connection.Command(sql)) |
Yours | Opened only if it was closed, then closed again only if the command opened it. Never disposed |
factory.Using(connection => ...) |
Created by Using |
Disposed when your action returns. Not opened for you |
// A closed connection is opened for the call and closed again afterwards.
connection.Close();
ConnectionState closedBefore = connection.State;
connection.Command("select 1").ExecuteScalar();
ConnectionState closedAfter = connection.State;
// A connection that was already open stays open.
connection.Open();
ConnectionState openBefore = connection.State;
connection.Command("select 1").ExecuteScalar();
ConnectionState openAfter = connection.State;
A command built from a single connection refuses concurrent terminals: a second call while one is running throws InvalidOperationException.
Open late, close early
Several helpers exist to keep the time a connection is open short:
EnsureOpen()andEnsureOpenAsync()open a closed (or broken) connection and return its prior state, so you can restore it.- Buffering helpers such as
Results<T>()queue the rows first and apply the transformation as you enumerate them. Map<T>()streams rows straight out of the reader while the connection is open; the connection is released as soon as the terminal returns.
Transactions
ExecuteTransactionConditional begins a transaction, runs your action, and commits only if the action returns true, nothing throws and the cancellation token was not cancelled. Otherwise it rolls back.
// The factory opens a connection and starts a transaction. Returning true commits; false (or a throw) rolls back.
bool committed = connectionFactory.Using(connection =>
connection.ExecuteTransactionConditional(transaction =>
{
transaction
.Command("insert into posts (id, author, title, body, views) values (100, 1, 'Temporary', null, 0)")
.ExecuteNonQuery();
return shouldCommit;
}));
Reader and command helpers
The foundation also includes helpers you can use without the expressive syntax: ForEach and ForEachAsync over a reader, Select and SelectAsync, AsEnumerable and AsAsyncEnumerable (rows as object[]), ToList, ToArray, ToDataTable and friends on commands. See the API reference for the full list.