tursodatabase/turso · error · ObjectDisposedException

ScalarFunctionRegistration

Error message

ScalarFunctionRegistration

What it means

The engine invoked a custom scalar function (CreateFunction) through the native trampoline, but the GCHandle for the managed ScalarFunctionRegistration has no live target: the registration was disposed while a statement still references the function. Registrations are torn down when the connection closes (FreeNativeFunctionContexts), so this indicates the connection died before a query calling the UDF finished. It is the scalar-function flavor of use-after-dispose across the interop boundary.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Functions.cs:102

        var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
        if (targetType == typeof(object))
            return (T)value;
        if (targetType == typeof(byte[]) && value is byte[] bytes)
            return (T)(object)bytes;
        if (targetType == typeof(string))
            return (T)(object)Convert.ToString(value, CultureInfo.InvariantCulture)!;
        if (targetType == typeof(bool))
            return (T)(object)Convert.ToBoolean(value, CultureInfo.InvariantCulture);

        return (T)Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);
    }

    private static TursoExtensionValue InvokeScalarFunction(IntPtr context, int argc, IntPtr argv, IntPtr contextDestructor, IntPtr valueDestructor)
    {
        try
        {
            var registration = (ScalarFunctionRegistration?)GCHandle.FromIntPtr(context).Target
                ?? throw new ObjectDisposedException(nameof(ScalarFunctionRegistration));
            var args = ReadArguments(argc, argv);
            return CreateResult(registration.Invoke(args));
        }
        catch (SqliteException ex)
        {
            return CreateError("__turso_sqlite_error__:" + ex.SqliteErrorCode.ToString(CultureInfo.InvariantCulture) + ":" + ex.Message);
        }
        catch (Exception ex)
        {
            return CreateError(ex.Message);
        }
    }

    private static void NoopContextDestructor(IntPtr context)
    {
    }

    private static void DestroyFunctionValue(IntPtr result)

View on GitHub (pinned to 6c72522679)

Solutions

  1. Keep the connection alive until every reader/command invoking the UDF has completed and been disposed.
  2. Materialize results (ToList()) before the owning scope disposes the connection.
  3. Register the function on each connection (CreateFunction is per-connection) and scope its lifetime to that connection's queries.
  4. Cancel queries via command cancellation rather than disposing the connection.

Example fix

// before
List<int> ids;
using (var conn = new SqliteConnection(cs))
{
    conn.Open();
    conn.CreateFunction<string, long>("len", s => s.Length);
    var cmd = new SqliteCommand("SELECT len(name) FROM t", conn);
    ids = ReadLazy(cmd.ExecuteReader()).ToList(); // reader escapes scope
}

// after
using (var conn = new SqliteConnection(cs))
{
    conn.Open();
    conn.CreateFunction<string, long>("len", s => s.Length);
    using var cmd = new SqliteCommand("SELECT len(name) FROM t", conn);
    using var reader = cmd.ExecuteReader();
    ids = ReadAll(reader); // consumed before conn disposes
}
Defensive patterns

Strategy: validation

Validate before calling

static List<T> ReadAll<T>(SqliteCommand cmd, Func<SqliteDataReader, T> map)
{
    if (cmd.Connection?.State != ConnectionState.Open)
        throw new InvalidOperationException("Connection must be open before invoking UDF queries.");
    using var reader = cmd.ExecuteReader();
    var results = new List<T>();
    while (reader.Read()) results.Add(map(reader));
    return results; // fully consumed before caller can dispose the connection
}

Try / catch

try
{
    var rows = ReadAll(cmd, MapRow);
}
catch (ObjectDisposedException ex) when (ex.ObjectName == "ScalarFunctionRegistration")
{
    throw new InvalidOperationException("UDF query outlived its connection; re-run on a fresh connection.", ex);
}

Prevention

When it happens

Trigger: Closing/disposing the SqliteConnection while a reader for a query that calls the UDF is still open; deferred LINQ evaluation escaping the connection's using scope; one thread disposing the connection while another executes a UDF query; long-running queries cancelled by tearing down the connection.

Common situations: 'using var conn' in a repository method returning a lazy sequence, unit tests that dispose fixtures while background queries run, and DI lifetimes disposing shared connections mid-request.

Related errors


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