Table of Contents

Connections

See how NextORM acquires, reuses and disposes the database connection, and how the connection-lifecycle role differs from the context itself.

Prerequisites: Quickstart · Dependency injection · Query reuse.

Overview

The SQL providers (SqliteDataContext, SqlServerDataContext, PostgresDataContext) all derive from DataContext, which implements both IDataContext and IConnectionManager. A context is built either from a connection string (the context creates and owns the connection) or from an already-built DbConnection (the caller owns it). The in-memory provider implements only IDataContext - it has no connection at all.

Connection string vs supplied connection

Every SQL provider exposes two constructors, and the Use… builder extensions wrap them:

// A: the context creates and owns the connection from the string.
using var owned = new SqliteDataContext("Data Source=app.db", new DataContextBuilder());

// B: the caller owns the connection; the context reuses this exact instance.
using var supplied = new SqliteConnection("Data Source=app.db");
using var reused = new SqliteDataContext(supplied, new DataContextBuilder());

// Equivalent through the builder:
var builderA = new DataContextBuilder().UseSqlite("app.db");
var builderB = new DataContextBuilder().UseSqlite(supplied);

ConnectionString returns the string the context was built with, or the supplied connection's own ConnectionString when the context was built from a connection:

using var ctx = new SqliteDataContext("Data Source=:memory:", new DataContextBuilder());
ctx.ConnectionString; // "Data Source=:memory:"

using var supplied = new SqliteConnection("Data Source=:memory:");
using var ctx2 = new SqliteDataContext(supplied, new DataContextBuilder());
ctx2.ConnectionString; // supplied.ConnectionString

Connection reuse

GetConnection creates the connection on first access and returns the same instance on every subsequent call. EnsureConnectionOpen opens it if it is closed; EnsureConnectionOpenAsync(CancellationToken) is the cancellable asynchronous form.

using var ctx = new SqliteDataContext("Data Source=app.db", new DataContextBuilder());

ctx.EnsureConnectionOpen();

bool same = ReferenceEquals(ctx.GetConnection(), ctx.GetConnection()); // true

Because the connection is reused, a supplied connection is the way to keep a per-connection database alive. A SQLite :memory: database is scoped to its connection, so a table created through a supplied connection is visible to the context only because the context runs on that very connection:

using var supplied = new SqliteConnection("Data Source=:memory:");
supplied.Open();

using (var setup = supplied.CreateCommand())
{
    setup.CommandText = "create table simple_entity (id integer primary key);" +
                        "insert into simple_entity (id) values (42);";
    setup.ExecuteNonQuery();
}

using var ctx = new SqliteDataContext(supplied, new DataContextBuilder());
var rows = ctx.From<ISimpleEntity>().Select(x => x.Id).ToList(); // [42]

Disposal rules

  • An owned connection (created from a connection string) is disposed by Dispose() / DisposeAsync(). Cached prepared commands that were bound to it are reset so they rebind on the next use.
  • A supplied connection is never disposed by the context - it is left in whatever state it was in. The caller owns it.
var owned = new SqliteDataContext("Data Source=:memory:", new DataContextBuilder());
var ownedConn = owned.GetConnection();
owned.EnsureConnectionOpen();
owned.Dispose();
// ownedConn.State == ConnectionState.Closed

using var supplied = new SqliteConnection("Data Source=:memory:");
supplied.Open();
using (var ctx = new SqliteDataContext(supplied, new DataContextBuilder()))
{
    ctx.EnsureConnectionOpen();
}
// supplied.State == ConnectionState.Open

Because IDataContext is registered as scoped in DI and implements IDisposable/IAsyncDisposable, disposing the scope disposes an owned connection automatically. See Dependency injection.

IConnectionManager

Connection lifecycle is a separate role, deliberately not part of IDataContext, so a provider without a connection (the in-memory context) is not forced to implement a no-op:

public interface IConnectionManager
{
    DbConnection GetConnection();
    void EnsureConnectionOpen();
    Task EnsureConnectionOpenAsync(CancellationToken cancellationToken = default);
}

DataContext implements it, so casting a context yields the same connection as the public GetConnection:

using var ctx = new SqliteDataContext("Data Source=app.db", new DataContextBuilder());
bool same = ReferenceEquals(((IConnectionManager)ctx).GetConnection(), ctx.GetConnection()); // true

Transactions

A transaction is a separate role too, so a context without a connection (the in-memory one) does not implement it. DataContext implements ITransactionManager: start a transaction with BeginTransaction*, or enlist in one owned by EF Core, Dapper or raw ADO.NET with UseTransaction. Savepoints (CreateSavepoint*/RollbackToSavepoint*/ReleaseSavepoint*) run on the active transaction or on one enlisted with UseTransaction, and never change its ownership. SQLite, PostgreSQL, SQL Server and MySQL/MariaDB support it; ClickHouse rejects it and the in-memory provider does not implement it. See Transactions for the full guide.

Command timeout

Every command nextorm executes exposes the ADO.NET DbCommand.CommandTimeout (seconds). The default is the provider's: nextorm does not touch the property unless you configure a timeout.

Set a context-wide default with UseCommandTimeout; it applies to every SELECT, DML and ExecuteScalar command of the contexts the builder creates:

using var ctx = new DataContextBuilder()
    .UseSqlite("app.db")
    .UseCommandTimeout(15)
    .CreateDataContext();

Override it for a single query with WithCommandTimeout(seconds) on the builder or on the built command (EntityBuilder<TEntity>, EntityBuilder and QueryCommand<TResult>):

var ids = ctx.From<ISimpleEntity>()
    .WithCommandTimeout(5)          // this query only
    .Select(x => x.Id)
    .ToList();

var slowIds = ctx.From<ISimpleEntity>()
    .Select(x => x.Id)
    .WithCommandTimeout(120)        // on QueryCommand<T> too
    .ToList();

Notes:

  • No call (null) and a value of zero or less keep the provider default; the property is not changed, so there is no cost when the setting is unused. WithCommandTimeout(0) clears the per-query override and inherits the context default.
  • The resolved value is part of the plan-cache key, so two otherwise identical queries with different timeouts do not share a cached command and the timeout never leaks from one query to the next. Contexts with different UseCommandTimeout values do not share a cached plan either.
  • The per-query override is available on read queries; DML commands and batches always use the context default.
  • The in-memory context has no command and ignores the setting.
  • Provider caveat: ClickHouse.Driver 1.4.0 exposes the inherited property, but an HTTP request is bounded by the connection Timeout (default two minutes), not CommandTimeout; the value has no effect there. See Limitations.

Provider differences

Provider Connection type Notes
SQLite SqliteConnection OnConnectionCreated registers the custom aggregate functions (stdev, stdevp, var, varp) for both created and supplied connections.
SQL Server SqlConnection No connection callback; a null parameter value is sent as DBNull.
PostgreSQL NpgsqlConnection No connection callback; a null parameter value is sent as DBNull.
MySQL MySqlConnection No connection callback; a null parameter value is sent as DBNull.
MariaDB MySqlConnection Uses the MySQL driver and dialect; a null parameter value is sent as DBNull.
ClickHouse ClickHouseConnection Parameters are sent as HTTP query parameters; a null value cannot have its ClickHouse type inferred, so set an explicit driver-level parameter type.
In-memory none InMemoryDataContext implements IDataContext only and does not expose GetConnection/IConnectionManager.

See also


Source: tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:24 (created connection), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:32 (stable across calls), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:40 (supplied instance), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:49 (supplied stays open), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:63 (owned is disposed), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:76 / :84 (ConnectionString), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:93 (IConnectionManager), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:101 (SQLite functions on a supplied connection), tests/nextorm.sqlite.tests/Raw/ConnectionManagementTests.cs:116 (queries use the supplied connection); src/nextorm.core/DataContext/Roles/IConnectionManager.cs:10, src/nextorm.core/DataContext/Roles/IContextEnvironment.cs:8, src/nextorm.core/DataContext/DataContext.cs:106 (GetConnection), src/nextorm.core/DataContext/DataContext.cs:176 (ConnectionString).