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
- Re-create collations on every connection instance after Open (CreateCollation is per-connection state, not global).
- Ensure the connection that registered the collation stays open for the full duration of any query that sorts/compares with it.
- Materialize query results before disposing the owning connection.
- 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
- Register collations right after every Open; they are per-connection state.
- If a column is declared COLLATE <custom>, register that collation on every connection before querying the table.
- Keep the registering connection open for the full duration of queries that use the collation.
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
- Cannot access a disposed object. Object name: 'AggregateInv
- ScalarFunctionRegistration
- Encryption is not supported by {libraryName}.
- Missing parameter values for {parameters}.
- Parameter name {parameterName} is ambiguous.
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/7296633c9d400d1b.
Report an issue: GitHub.