{"record":{"id":"d85d408077c45c45","repo":"dotnet/efcore","slug":"fromsql-or-sqlquery-was-called-with-non-compos","errorCode":null,"errorMessage":"'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side.","messagePattern":"'FromSql' or 'SqlQuery' was called with non-composable SQL and with a query composing over it\\. Consider calling 'AsEnumerable' after the method to perform the composition on the client side\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/EFCore.Relational/Query/QuerySqlGenerator.cs","lineNumber":567,"sourceCode":"    /// <summary>\n    ///     Checks whether a given SQL string is composable, i.e. can be embedded as a subquery within a\n    ///     larger SQL query.\n    /// </summary>\n    /// <param name=\"sql\">An SQL string to be checked for composability.</param>\n    /// <exception cref=\"InvalidOperationException\">The given SQL isn't composable.</exception>\n    protected virtual void CheckComposableSql(string sql)\n    {\n        var span = sql.AsSpan().TrimStart();\n\n        while (true)\n        {\n            // SQL -- comment\n            if (span.StartsWith(\"--\"))\n            {\n                var i = span.IndexOf('\\n');\n                span = i > 0\n                    ? span[(i + 1)..].TrimStart()\n                    : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);\n                continue;\n            }\n\n            // SQL /* */ comment\n            if (span.StartsWith(\"/*\"))\n            {\n                var i = span.IndexOf(\"*/\");\n                span = i > 0\n                    ? span[(i + 2)..].TrimStart()\n                    : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);\n                continue;\n            }\n\n            break;\n        }\n\n        CheckComposableSqlTrimmed(span);\n    }","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/dotnet/efcore/blob/3a2006ef569de08368d59db5e1468aa8f407e4f8/src/EFCore.Relational/Query/QuerySqlGenerator.cs#L549-L585","documentation":"EF Core throws this from CheckComposableSql when it must embed your raw SQL as a subquery (i.e. you compose LINQ over FromSqlRaw/FromSqlInterpolated/SqlQueryRaw/SqlQueryInterpolated) but the SQL cannot be verified as composable. This specific throw site fires when the trimmed SQL begins a '--' line comment that is never terminated by a newline, so EF cannot find the actual statement that follows. EF composes by wrapping your SQL in (...) and projecting columns, so it must be a single SELECT/WITH statement.","triggerScenarios":"Calling context.Set<T>().FromSqlRaw(\"-- comment with no trailing newline\") followed by any LINQ operator (Where/Select/OrderBy/...). Also FromSqlInterpolated with the same shape, or SqlQueryRaw/SqlQueryInterpolated on DbContext. The composition forces VisitFromSql -> CheckComposableSql. Any '--' comment that runs to the end of the string with no '\\n' hits this branch (IndexOf('\\n') <= 0).","commonSituations":"Hand-written SQL strings built by concatenation where a trailing comment loses its newline; SQL copied from a profiler/SSMS that ends in a comment; templated SQL where a placeholder for the statement body resolves to empty. Encountered when migrating from raw ADO.NET to FromSql while keeping comments.","solutions":["Remove the unterminated trailing '--' comment, or add a newline after it so the real SELECT/WITH follows.","If the SQL is genuinely non-composable (stored proc, multi-statement), stop composing: append .AsEnumerable() (or .ToList()) immediately after FromSql*/SqlQuery* and do the remaining LINQ in memory.","Ensure the SQL, after comments are stripped, begins with SELECT or WITH followed by whitespace."],"exampleFix":"// before\nvar q = db.Blogs\n    .FromSqlRaw(\"-- top comment\" + sql)   // no '\\n' -> throws 580\n    .Where(b => b.Active);\n// after\nvar q = db.Blogs\n    .FromSqlRaw(\"-- top comment\\n\" + sql)\n    .Where(b => b.Active);","handlingStrategy":"validation","validationCode":"// Reject SQL with an unterminated '--' comment before FromSql*.\nstatic bool HasUnterminatedLineComment(string sql)\n{\n    var span = sql.AsSpan().TrimStart();\n    while (span.Length > 0)\n    {\n        if (span.StartsWith(\"--\"))\n        {\n            var nl = span.IndexOf('\\n');\n            if (nl <= 0) return true;          // unterminated\n            span = span[(nl + 1)..].TrimStart();\n        }\n        else if (span.StartsWith(\"/*\"))\n        {\n            var end = span.IndexOf(\"*/\");\n            if (end < 0) return true;\n            span = span[(end + 2)..].TrimStart();\n        }\n        else break;\n    }\n    return false;\n}\n\nif (HasUnterminatedLineComment(sql))\n    throw new ArgumentException(\"SQL has an unterminated '--' comment; EF will reject it as non-composable.\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Build raw SQL from templates that always terminate comments with a newline.","When composing LINQ over FromSql*/SqlQuery*, prefer a single SELECT/WITH statement and avoid inline comments.","If the SQL is non-composable, call .AsEnumerable()/.ToListAsync() immediately after FromSql*."],"tags":["efcore","fromsql","sql","query-translation"],"backgroundTag":null,"analyzedSha":"3a2006ef569de08368d59db5e1468aa8f407e4f8","analyzedAt":"2026-08-11T23:42:04.146Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}