tursodatabase/turso · error · ObjectDisposedException
Cannot access a disposed object. Object name: 'AggregateInv
Error message
Cannot access a disposed object. Object name: 'AggregateInvocation'.
What it means
The native side invoked the step callback of a custom aggregate function (CreateAggregate), but the GCHandle pointing to the managed AggregateInvocation no longer has a target: the invocation object was disposed while the statement was still executing. This is a use-after-dispose across the native/managed interop boundary, surfaced as ObjectDisposedException('AggregateInvocation'). It almost always means the connection (or the registration's native context) was closed/freed before the query consuming the aggregate finished.
Source
Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Aggregates.cs:80
private static object? InvokeSeededAggregateStep<TAccumulate>(Func<TAccumulate, object?[], TAccumulate> function, object? accumulator, object?[] args)
=> function((TAccumulate)accumulator!, args);
private static object? InvokeResultSelector<TAccumulate, TResult>(Func<TAccumulate, TResult> resultSelector, object? accumulator)
=> resultSelector((TAccumulate)accumulator!);
private static IntPtr InitializeAggregate(IntPtr context)
{
var registration = (AggregateFunctionRegistration?)GCHandle.FromIntPtr(context).Target
?? throw new ObjectDisposedException(nameof(AggregateFunctionRegistration));
return registration.CreateInvocationHandle();
}
private static TursoExtensionValue StepAggregate(IntPtr context, IntPtr aggregateContext, int argc, IntPtr argv)
{
try
{
var invocation = (AggregateInvocation?)GCHandle.FromIntPtr(aggregateContext).Target
?? throw new ObjectDisposedException(nameof(AggregateInvocation));
invocation.Step(ReadArguments(argc, argv));
return CreateResult(null);
}
catch (SqliteException ex)
{
return CreateError("__turso_sqlite_error__:" + ex.SqliteErrorCode.ToString(System.Globalization.CultureInfo.InvariantCulture) + ":" + ex.Message);
}
catch (Exception ex)
{
return CreateError(ex.Message);
}
}
private static TursoExtensionValue FinalizeAggregate(IntPtr context, IntPtr aggregateContext)
{
try
{
var invocation = (AggregateInvocation?)GCHandle.FromIntPtr(aggregateContext).TargetView on GitHub (pinned to 6c72522679)
Solutions
- Keep the connection alive until every reader/command using the custom aggregate has completed and been disposed.
- Materialize results (ToList()/ToArray()) before the owning connection's using scope exits.
- If multi-threaded, give each logical unit its own SqliteConnection instead of disposing a shared one.
- Audit that CreateAggregate registrations outlive every statement that references the aggregate.
Example fix
// before
IEnumerable<Row> rows;
using (var conn = new SqliteConnection(cs))
{
conn.Open();
conn.CreateAggregate<long, long>("total", (acc, x) => acc + x);
using var cmd = new SqliteCommand("SELECT total(v) FROM t", conn);
rows = ReadRows(cmd.ExecuteReader()); // deferred
} // conn disposed while reader still steps -> ObjectDisposedException
// after
using (var conn = new SqliteConnection(cs))
{
conn.Open();
conn.CreateAggregate<long, long>("total", (acc, x) => acc + x);
using var cmd = new SqliteCommand("SELECT total(v) FROM t", conn);
rows = ReadRows(cmd.ExecuteReader()).ToList(); // fully consumed inside the scope
} Defensive patterns
Strategy: validation
Validate before calling
static SqliteDataReader ExecuteFully(SqliteCommand cmd)
{
if (cmd.Connection?.State != ConnectionState.Open)
throw new InvalidOperationException("Connection must be open before executing an aggregate query.");
return cmd.ExecuteReader(); // caller MUST dispose reader inside the connection's lifetime
} Try / catch
try
{
var result = ReadAggregateQuery(conn);
}
catch (ObjectDisposedException ex) when (ex.ObjectName is "AggregateInvocation" or "AggregateFunctionRegistration")
{
// Connection was torn down mid-query: restart on a fresh connection instead of reusing state.
throw new InvalidOperationException("Query outlived its connection; re-run on a new connection.", ex);
} Prevention
- Materialize query results (ToList/ToArray) before disposing the owning connection.
- Wrap reader and command in 'using' inside the connection's 'using' so finalization happens while alive.
- Never dispose a connection from another thread to cancel a query; use command cancellation.
- Register aggregates per connection and treat the registration's lifetime as the connection's lifetime.
When it happens
Trigger: Calling connection.Close() or Dispose() while a reader iterating a query that uses a custom aggregate is still open; a 'using var conn' scope ending before deferred LINQ evaluation over an open reader runs; one thread disposing the connection while another executes the aggregate; statements left un-disposed that are stepped after connection teardown.
Common situations: Returning IQueryable/deferred sequences from a repository that owns the connection in a using block, fire-and-forget tasks racing connection disposal, and sharing one connection across async flows where the first to finish disposes it.
Related errors
- ScalarFunctionRegistration
- CollationRegistration
- Function {function} was called with a NULL value at ordinal
- Encryption is not supported by {libraryName}.
- Missing parameter values for {parameters}.
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/97c9eaf66b2ddd54.
Report an issue: GitHub.