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).Target

View on GitHub (pinned to 6c72522679)

Solutions

  1. Keep the connection alive until every reader/command using the custom aggregate has completed and been disposed.
  2. Materialize results (ToList()/ToArray()) before the owning connection's using scope exits.
  3. If multi-threaded, give each logical unit its own SqliteConnection instead of disposing a shared one.
  4. 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

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


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20). Data as JSON: /api/errors/97c9eaf66b2ddd54. Report an issue: GitHub.