tursodatabase/turso · error · ObjectDisposedException

CollationRegistration

Error message

CollationRegistration

What it means

The native collation comparison callback (registered via CreateCollation) was invoked, but the GCHandle for the managed CollationRegistration no longer resolves to a live object. This happens when the registration was torn down (connection close frees the native function contexts) while the engine still needs the collation to compare strings, e.g. for a still-running query or an index created with that collation. It is the collation flavor of use-after-dispose across the interop boundary.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Collations.cs:45

        var registration = new CollationRegistration(name, comparison);
        _collations[name] = registration;
        if (HasNativeCallbackHandle)
        {
            using var syncOperation = _managedConnection?.EnterSyncOperation();
            _nativeFunctionContexts.Add(registration.Register(DatabaseHandle));
        }
    }

    private void RegisterCollations()
    {
        foreach (var registration in _collations.Values)
            _nativeFunctionContexts.Add(registration.Register(DatabaseHandle));
    }

    private static int InvokeCollation(IntPtr context, IntPtr leftPtr, UIntPtr leftLen, IntPtr rightPtr, UIntPtr rightLen)
    {
        var registration = (CollationRegistration?)GCHandle.FromIntPtr(context).Target
            ?? throw new ObjectDisposedException(nameof(CollationRegistration));
        return registration.Compare(ReadUtf8(leftPtr, checked((int)leftLen)), ReadUtf8(rightPtr, checked((int)rightLen)));
    }

    private static string ReadUtf8(IntPtr ptr, int length)
    {
        if (ptr == IntPtr.Zero || length == 0)
            return string.Empty;

        var bytes = new byte[length];
        Marshal.Copy(ptr, bytes, 0, bytes.Length);
        return Encoding.UTF8.GetString(bytes);
    }

    private sealed class CollationRegistration(string name, Func<string, string, int> compare)
    {
        public int Compare(string left, string right) => compare(left, right);

        public GCHandle Register(Turso.Raw.Public.Handles.TursoDatabaseHandle database)

View on GitHub (pinned to 6c72522679)

Solutions

  1. Re-create collations on every connection instance after Open (CreateCollation is per-connection state, not global).
  2. Ensure the connection that registered the collation stays open for the full duration of any query that sorts/compares with it.
  3. Materialize query results before disposing the owning connection.
  4. If a column declares a custom collation, register that collation on every connection touching the table before querying it.

Example fix

// before
using (var conn = new SqliteConnection(cs))
{
    conn.Open();
    conn.CreateCollation("NOCASE_WS", (a, b) => string.Compare(a.Trim(), b.Trim()));
} // connection gone
using (var conn2 = new SqliteConnection(cs))
{
    conn2.Open();
    // later engine callback finds the disposed CollationRegistration
    Execute("SELECT * FROM t ORDER BY name COLLATE NOCASE_WS", conn2);
}

// after
using (var conn2 = new SqliteConnection(cs))
{
    conn2.Open();
    conn2.CreateCollation("NOCASE_WS", (a, b) => string.Compare(a.Trim(), b.Trim())); // re-register per connection
    Execute("SELECT * FROM t ORDER BY name COLLATE NOCASE_WS", conn2);
}
Defensive patterns

Strategy: validation

Validate before calling

static SqliteConnection OpenWithCollations(string cs, Action<SqliteConnection> register)
{
    var conn = new SqliteConnection(cs);
    conn.Open();
    register(conn); // re-register every collation on every connection instance
    return conn;
}

// usage:
// using var conn = OpenWithCollations(cs, c => c.CreateCollation("NOCASE_WS", (a,b) => string.Compare(a.Trim(), b.Trim())));

Try / catch

try
{
    var rows = ReadSorted(conn);
}
catch (ObjectDisposedException ex) when (ex.ObjectName == "CollationRegistration")
{
    // Collation gone (connection closed): reopen, re-register, retry once
    throw new InvalidOperationException("Collation outlived its connection; re-register and retry.", ex);
}

Prevention

When it happens

Trigger: Closing/disposing the connection while a query whose ORDER BY / WHERE / index uses a custom collation is still executing; a table having a column declared with a custom collation, then queries run after the connection that registered the collation was closed and re-opened without re-registering; disposing registrations from another thread mid-query.

Common situations: Registering collations once in app startup on a short-lived connection instead of per-connection, deferred LINQ execution escaping the connection's using scope, and connection-pool-style reuse where a second connection lacks the registration.

Related errors


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