{"record":{"id":"9c32cc954e54bcd1","repo":"tursodatabase/turso","slug":"scalarfunctionregistration","errorCode":null,"errorMessage":"ScalarFunctionRegistration","messagePattern":"ScalarFunctionRegistration","errorType":"exception","errorClass":"ObjectDisposedException","httpStatus":null,"severity":"error","filePath":"bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Functions.cs","lineNumber":102,"sourceCode":"        var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);\n        if (targetType == typeof(object))\n            return (T)value;\n        if (targetType == typeof(byte[]) && value is byte[] bytes)\n            return (T)(object)bytes;\n        if (targetType == typeof(string))\n            return (T)(object)Convert.ToString(value, CultureInfo.InvariantCulture)!;\n        if (targetType == typeof(bool))\n            return (T)(object)Convert.ToBoolean(value, CultureInfo.InvariantCulture);\n\n        return (T)Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture);\n    }\n\n    private static TursoExtensionValue InvokeScalarFunction(IntPtr context, int argc, IntPtr argv, IntPtr contextDestructor, IntPtr valueDestructor)\n    {\n        try\n        {\n            var registration = (ScalarFunctionRegistration?)GCHandle.FromIntPtr(context).Target\n                ?? throw new ObjectDisposedException(nameof(ScalarFunctionRegistration));\n            var args = ReadArguments(argc, argv);\n            return CreateResult(registration.Invoke(args));\n        }\n        catch (SqliteException ex)\n        {\n            return CreateError(\"__turso_sqlite_error__:\" + ex.SqliteErrorCode.ToString(CultureInfo.InvariantCulture) + \":\" + ex.Message);\n        }\n        catch (Exception ex)\n        {\n            return CreateError(ex.Message);\n        }\n    }\n\n    private static void NoopContextDestructor(IntPtr context)\n    {\n    }\n\n    private static void DestroyFunctionValue(IntPtr result)","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/tursodatabase/turso/blob/6c7252267988c76e632af00a671e4b9788dfae13/bindings/dotnet/src/Turso.Data.Sqlite/SqliteConnection.Functions.cs#L84-L120","documentation":"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.","triggerScenarios":"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.","commonSituations":"'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.","solutions":["Keep the connection alive until every reader/command invoking the UDF has completed and been disposed.","Materialize results (ToList()) before the owning scope disposes the connection.","Register the function on each connection (CreateFunction is per-connection) and scope its lifetime to that connection's queries.","Cancel queries via command cancellation rather than disposing the connection."],"exampleFix":"// before\nList<int> ids;\nusing (var conn = new SqliteConnection(cs))\n{\n    conn.Open();\n    conn.CreateFunction<string, long>(\"len\", s => s.Length);\n    var cmd = new SqliteCommand(\"SELECT len(name) FROM t\", conn);\n    ids = ReadLazy(cmd.ExecuteReader()).ToList(); // reader escapes scope\n}\n\n// after\nusing (var conn = new SqliteConnection(cs))\n{\n    conn.Open();\n    conn.CreateFunction<string, long>(\"len\", s => s.Length);\n    using var cmd = new SqliteCommand(\"SELECT len(name) FROM t\", conn);\n    using var reader = cmd.ExecuteReader();\n    ids = ReadAll(reader); // consumed before conn disposes\n}","handlingStrategy":"validation","validationCode":"static List<T> ReadAll<T>(SqliteCommand cmd, Func<SqliteDataReader, T> map)\n{\n    if (cmd.Connection?.State != ConnectionState.Open)\n        throw new InvalidOperationException(\"Connection must be open before invoking UDF queries.\");\n    using var reader = cmd.ExecuteReader();\n    var results = new List<T>();\n    while (reader.Read()) results.Add(map(reader));\n    return results; // fully consumed before caller can dispose the connection\n}","typeGuard":null,"tryCatchPattern":"try\n{\n    var rows = ReadAll(cmd, MapRow);\n}\ncatch (ObjectDisposedException ex) when (ex.ObjectName == \"ScalarFunctionRegistration\")\n{\n    throw new InvalidOperationException(\"UDF query outlived its connection; re-run on a fresh connection.\", ex);\n}","preventionTips":["Scope 'using var conn' outside every 'using var cmd' / 'using var reader' that calls UDFs.","Materialize deferred sequences before leaving the connection's scope.","Register functions per connection; do not assume registrations survive close/open."],"tags":["ado-net","sqlite","turso","user-defined-functions","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"}