dotnet/efcore · error · InvalidOperationException
FromSqlNonComposable
FromSqlNonComposable
Error message
'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.
What it means
Thrown by CheckComposableSql when a FromSqlInterpolated/FromSqlRaw SQL string contains a SQL line comment ('--') that is never terminated by a newline. Because EF needs to embed the raw SQL as a subquery (it wraps it in '(...) AS t') to compose LINQ operators over it, it must verify the leading token is composable; an unterminated comment makes that impossible to determine. This only triggers when additional LINQ operators are composed over the FromSql query (forcing VisitFromSql/CheckComposableSql to run).
Source
Thrown at src/EFCore.Relational/Query/QuerySqlGenerator.cs:567
/// <summary>
/// Checks whether a given SQL string is composable, i.e. can be embedded as a subquery within a
/// larger SQL query.
/// </summary>
/// <param name="sql">An SQL string to be checked for composability.</param>
/// <exception cref="InvalidOperationException">The given SQL isn't composable.</exception>
protected virtual void CheckComposableSql(string sql)
{
var span = sql.AsSpan().TrimStart();
while (true)
{
// SQL -- comment
if (span.StartsWith("--"))
{
var i = span.IndexOf('\n');
span = i > 0
? span[(i + 1)..].TrimStart()
: throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);
continue;
}
// SQL /* */ comment
if (span.StartsWith("/*"))
{
var i = span.IndexOf("*/");
span = i > 0
? span[(i + 2)..].TrimStart()
: throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable);
continue;
}
break;
}
CheckComposableSqlTrimmed(span);
}View on GitHub (pinned to dbf9771522)
Solutions
- Ensure any '--' line comment is followed by a newline ('\n') so EF can skip past it.
- Remove the inline comment from the raw SQL string.
- If the SQL is genuinely non-composable (e.g. a stored proc call), call AsEnumerable()/AsAsyncEnumerable() immediately after FromSql so composition happens client-side and CheckComposableSql is never invoked.
- Refactor the comment into a C# comment above the FromSql call instead of inside the SQL literal.
Example fix
// before
var q = db.Blogs.FromSqlRaw("-- top secret\nSELECT * FROM Blogs").Where(b => b.Id > 0);
// the above throws only if the literal lacks a newline; fix by terminating the comment:
var q = db.Blogs.FromSqlRaw("-- top secret\r\nSELECT * FROM Blogs").Where(b => b.Id > 0);
// or move the note out of SQL:
// top secret
var q = db.Blogs.FromSqlRaw("SELECT * FROM Blogs").Where(b => b.Id > 0); Defensive patterns
Strategy: validation
Validate before calling
static bool IsComposableSql(string sql)
{
var i = 0;
while (i < sql.Length)
{
if (sql[i] == '-' && i + 1 < sql.Length && sql[i + 1] == '-')
{
var nl = sql.IndexOf('\n', i);
if (nl < 0) return false; // unterminated line comment
i = nl + 1;
}
else if (sql[i] == '/' && i + 1 < sql.Length && sql[i + 1] == '*')
{
var close = sql.IndexOf("*/", i, StringComparison.Ordinal);
if (close < 0) return false; // unterminated block comment
i = close + 2;
}
else { i++; }
}
return true;
}
if (!IsComposableSql(rawSql)) rawSql += "\n"; // or call AsEnumerable() instead Try / catch
try { var r = db.Blogs.FromSqlRaw(sql).Where(b => b.Id > 0).ToList(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("non-composable SQL"))
{
// fall back to client-side composition
var r = db.Blogs.FromSqlRaw(sql).AsEnumerable().Where(b => b.Id > 0).ToList();
} Prevention
- Never end a FromSql SQL string with a line comment; always terminate '--' with a newline.
- Centralize raw-SQL construction in one helper that appends a trailing newline.
- Unit-test raw SQL fragments for composability before passing them to FromSql.
- Prefer FromSqlInterpolated over string concatenation to avoid accidental comment truncation.
When it happens
Trigger: Calling FromSqlRaw/FromSqlInterpolated with a string that begins with or contains a '--' comment with no trailing '\n', then chaining a LINQ operator (e.g. .Where(), .Select(), .ToList()) so EF must treat it as a subquery. Example: context.Blogs.FromSqlRaw("-- my query SELECT * FROM Blogs").ToList() with the comment lacking a final newline.
Common situations: Building SQL strings dynamically and concatenating a comment header without a trailing newline; reading SQL from a resource/external source where trailing whitespace was stripped; copy-pasting a commented query fragment; using string interpolation that drops the terminating newline.
Related errors
- The required column '{column}' was not present in the result
- SelectExpressionNonTphWithCustomTable
- A FromSqlExpression has an invalid arguments expression type
- The expression '{sqlExpression}' in the SQL tree does not ha
- The replacement entity type: {entityType} does not have same
AI-assisted analysis of dotnet/efcore@dbf9771522 (2026-08-06).
Data as JSON: /api/errors/bea85978c17c6486.
Report an issue: GitHub.