The ADO.NET provider is the layer where you write the SQL yourself. Everything is where you expect it to be: DbConnection, DbCommand, DbDataReader, DbTransaction, DbParameter, a data adapter, a command builder and a provider factory.

Base class Here
DbConnection WitDbConnection
DbCommand WitDbCommand
DbDataReader WitDbDataReader
DbTransaction WitDbTransaction
DbParameter WitDbParameter
DbParameterCollection WitDbParameterCollection
DbConnectionStringBuilder WitDbConnectionStringBuilder
DbDataAdapter WitDbDataAdapter
DbCommandBuilder WitDbCommandBuilder
DbProviderFactory WitDbProviderFactory

The basics

csharp
using OutWit.Database.AdoNet;

using var connection = new WitDbConnection("Data Source=app.witdb");
connection.Open();

using var command = connection.CreateCommand();
command.CommandText = "SELECT Id, Name FROM Users WHERE IsActive = @active";
command.Parameters.AddWithValue("@active", true);

using var reader = command.ExecuteReader();

while (reader.Read())
    Console.WriteLine($"{reader.GetInt64(0)}: {reader.GetString(1)}");

The async equivalents are all there: OpenAsync, ExecuteReaderAsync, ExecuteNonQueryAsync, ExecuteScalarAsync, ReadAsync.

Parameters

Named parameters, prefixed with @. Two ways to add one, both fine:

csharp
command.Parameters.AddWithValue("@name", "Alice");
command.Parameters.Add(new WitDbParameter("@name", DbType.String, 100));

Types are inferred from the value when you do not say. Being explicit is worth it where the inference could go either way, such as a null whose column type matters.

Use parameters rather than building the statement by concatenation. Beyond the injection question, a parameterised statement can be prepared once and reused.

Prepared statements

Prepare() parses and plans the statement and keeps the result on the command. Run the same statement many times with different values and you pay the parse once.

csharp
command.CommandText = "INSERT INTO Events (Name, At) VALUES (@name, @at)";
command.Parameters.Add(new WitDbParameter("@name", DbType.String));
command.Parameters.Add(new WitDbParameter("@at", DbType.DateTime));
command.Prepare();

foreach (var e in events)
{
    command.Parameters["@name"].Value = e.Name;
    command.Parameters["@at"].Value = e.At;
    command.ExecuteNonQuery();
}

Changing CommandText discards the prepared statement, so a command reused for different SQL behaves correctly rather than quietly running the old plan.

Stored procedures

CommandType.StoredProcedure works the way it does everywhere else: the routine name goes in CommandText, the parameters go in the collection.

csharp
using var command = connection.CreateCommand();
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "ArchiveOrdersBefore";
command.Parameters.AddWithValue("@cutoff", DateTime.UtcNow.AddYears(-1));

command.ExecuteNonQuery();

CommandType.TableDirect is refused. It means "the command text is a table name, return all of it", which this provider has no translation for, and refusing it beats answering something approximate.

See WitSQL for writing the routines themselves.

Transactions

csharp
using var transaction = connection.BeginTransaction(IsolationLevel.Snapshot);

using var command = connection.CreateCommand();
command.Transaction = transaction;

// ... statements

transaction.Commit();

Assign the transaction to the command. A command without one runs in autocommit, which for a large write is worth avoiding; see transactions.

Schema metadata

GetSchema answers the standard collections, so tooling that inspects a database generically works against this one.

csharp
var tables = connection.GetSchema("Tables");
var columns = connection.GetSchema("Columns", new[] { null, null, "Users" });
Collection Holds
MetaDataCollections What this list contains
DataSourceInformation Identifier quoting, parameter marker, version
DataTypes The type names and their CLR mappings
Restrictions What each collection can be filtered by
ReservedWords The words that need quoting
Tables, Columns, Views The schema
Indexes, IndexColumns, ForeignKeys The rest of it

For the same information in SQL, see INFORMATION_SCHEMA.

ChangeDatabase throws. One connection is one database, and switching would have to mean something this engine has no concept of.

DataAdapter and CommandBuilder

For code built around DataTable, the adapter fills and updates as usual, and the command builder generates the insert, update and delete from your select:

csharp
using var adapter = new WitDbDataAdapter("SELECT Id, Name, Email FROM Users", connection);
using var builder = new WitDbCommandBuilder(adapter);

var table = new DataTable();
adapter.Fill(table);

table.Rows[0]["Name"] = "Updated";

adapter.Update(table);

The command builder needs a single-table select that includes the primary key, and a table that has one. Anything more complicated means writing the three commands yourself, which the adapter accepts directly.

Provider factory

For code that picks its database at runtime:

csharp
DbProviderFactories.RegisterFactory(
    WitDbProviderFactory.PROVIDER_INVARIANT_NAME,
    WitDbProviderFactory.Instance);

var factory = DbProviderFactories.GetFactory(WitDbProviderFactory.PROVIDER_INVARIANT_NAME);

using var connection = factory.CreateConnection()!;
connection.ConnectionString = configuration.GetConnectionString("Default");
connection.Open();

Use the constant rather than typing the name. It is OutWit.Database.AdoNet, and registering under one string while asking for another is an easy afternoon to lose.

Where to go next