dotnet/efcore · error · InvalidOperationException

'FromSql' or 'SqlQuery' was called with non-composable SQL a

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

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.

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 3a2006ef56)

Solutions

  1. Remove the unterminated trailing '--' comment, or add a newline after it so the real SELECT/WITH follows.
  2. 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.
  3. Ensure the SQL, after comments are stripped, begins with SELECT or WITH followed by whitespace.

Example fix

// before
var q = db.Blogs
    .FromSqlRaw("-- top comment" + sql)   // no '\n' -> throws 580
    .Where(b => b.Active);
// after
var q = db.Blogs
    .FromSqlRaw("-- top comment\n" + sql)
    .Where(b => b.Active);
Defensive patterns

Strategy: validation

Validate before calling

// Reject SQL with an unterminated '--' comment before FromSql*.
static bool HasUnterminatedLineComment(string sql)
{
    var span = sql.AsSpan().TrimStart();
    while (span.Length > 0)
    {
        if (span.StartsWith("--"))
        {
            var nl = span.IndexOf('\n');
            if (nl <= 0) return true;          // unterminated
            span = span[(nl + 1)..].TrimStart();
        }
        else if (span.StartsWith("/*"))
        {
            var end = span.IndexOf("*/");
            if (end < 0) return true;
            span = span[(end + 2)..].TrimStart();
        }
        else break;
    }
    return false;
}

if (HasUnterminatedLineComment(sql))
    throw new ArgumentException("SQL has an unterminated '--' comment; EF will reject it as non-composable.");

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of dotnet/efcore@3a2006ef56 (2026-08-11). Data as JSON: /api/errors/d85d408077c45c45. Report an issue: GitHub.