{"record":{"id":"7296633c9d400d1b","repo":"tursodatabase/turso","slug":"collationregistration","errorCode":null,"errorMessage":"CollationRegistration","messagePattern":"CollationRegistration","errorType":"exception","errorClass":"ObjectDisposedException","httpStatus":null,"severity":"error","filePath":"bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Collations.cs","lineNumber":45,"sourceCode":"        var registration = new CollationRegistration(name, comparison);\n        _collations[name] = registration;\n        if (HasNativeCallbackHandle)\n        {\n            using var syncOperation = _managedConnection?.EnterSyncOperation();\n            _nativeFunctionContexts.Add(registration.Register(DatabaseHandle));\n        }\n    }\n\n    private void RegisterCollations()\n    {\n        foreach (var registration in _collations.Values)\n            _nativeFunctionContexts.Add(registration.Register(DatabaseHandle));\n    }\n\n    private static int InvokeCollation(IntPtr context, IntPtr leftPtr, UIntPtr leftLen, IntPtr rightPtr, UIntPtr rightLen)\n    {\n        var registration = (CollationRegistration?)GCHandle.FromIntPtr(context).Target\n            ?? throw new ObjectDisposedException(nameof(CollationRegistration));\n        return registration.Compare(ReadUtf8(leftPtr, checked((int)leftLen)), ReadUtf8(rightPtr, checked((int)rightLen)));\n    }\n\n    private static string ReadUtf8(IntPtr ptr, int length)\n    {\n        if (ptr == IntPtr.Zero || length == 0)\n            return string.Empty;\n\n        var bytes = new byte[length];\n        Marshal.Copy(ptr, bytes, 0, bytes.Length);\n        return Encoding.UTF8.GetString(bytes);\n    }\n\n    private sealed class CollationRegistration(string name, Func<string, string, int> compare)\n    {\n        public int Compare(string left, string right) => compare(left, right);\n\n        public GCHandle Register(Turso.Raw.Public.Handles.TursoDatabaseHandle database)","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/tursodatabase/turso/blob/6c7252267988c76e632af00a671e4b9788dfae13/bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Collations.cs#L27-L63","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nusing (var conn = new SqliteConnection(cs))\n{\n    conn.Open();\n    conn.CreateCollation(\"NOCASE_WS\", (a, b) => string.Compare(a.Trim(), b.Trim()));\n} // connection gone\nusing (var conn2 = new SqliteConnection(cs))\n{\n    conn2.Open();\n    // later engine callback finds the disposed CollationRegistration\n    Execute(\"SELECT * FROM t ORDER BY name COLLATE NOCASE_WS\", conn2);\n}\n\n// after\nusing (var conn2 = new SqliteConnection(cs))\n{\n    conn2.Open();\n    conn2.CreateCollation(\"NOCASE_WS\", (a, b) => string.Compare(a.Trim(), b.Trim())); // re-register per connection\n    Execute(\"SELECT * FROM t ORDER BY name COLLATE NOCASE_WS\", conn2);\n}","handlingStrategy":"validation","validationCode":"static SqliteConnection OpenWithCollations(string cs, Action<SqliteConnection> register)\n{\n    var conn = new SqliteConnection(cs);\n    conn.Open();\n    register(conn); // re-register every collation on every connection instance\n    return conn;\n}\n\n// usage:\n// using var conn = OpenWithCollations(cs, c => c.CreateCollation(\"NOCASE_WS\", (a,b) => string.Compare(a.Trim(), b.Trim())));","typeGuard":null,"tryCatchPattern":"try\n{\n    var rows = ReadSorted(conn);\n}\ncatch (ObjectDisposedException ex) when (ex.ObjectName == \"CollationRegistration\")\n{\n    // Collation gone (connection closed): reopen, re-register, retry once\n    throw new InvalidOperationException(\"Collation outlived its connection; re-register and retry.\", ex);\n}","preventionTips":["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."],"tags":["ado-net","sqlite","turso","collation","use-after-dispose","interop"],"backgroundTag":"use-after-dispose","analyzedSha":"6c7252267988c76e632af00a671e4b9788dfae13","analyzedAt":"2026-08-20T07:02:18.389Z","contentChangedAt":"2026-08-20T07:02:18.389Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}