Learnixo
Back to blog
AI Systemsintermediate

EF Core Raw SQL — FromSqlRaw, ExecuteSqlRaw, and Dapper Integration

Execute raw SQL in EF Core: FromSqlRaw for entity queries, ExecuteSqlRaw for commands, SqlQuery for arbitrary projections, safe parameterization to prevent SQL injection, and when to drop to Dapper.

Asma Hafeez KhanMay 16, 20264 min read
EF CoreRaw SQLSQLASP.NET Core.NETSecurity
Share:𝕏

When Raw SQL is Justified

EF Core LINQ is the first choice — it is type-safe and parameterized automatically.

Raw SQL is justified when:
  ✓ Complex queries that LINQ cannot translate (e.g., recursive CTEs, PIVOT)
  ✓ Stored procedures that return entity-shaped results
  ✓ Bulk operations (UPDATE all rows matching condition without loading them)
  ✓ Window functions (ROW_NUMBER, RANK, LAG/LEAD)
  ✓ Performance-critical queries where you need the exact SQL plan

Raw SQL is NOT justified when:
  ✗ "LINQ is confusing" — learn LINQ, do not escape to raw SQL
  ✗ Simple WHERE conditions — LINQ handles these cleanly
  ✗ Anywhere you concatenate user input into a query string (SQL injection risk)

FromSqlRaw — Query for Entities

C#
// Query patients using raw SQL, EF Core maps result to Patient entities
// Parameters MUST use SqlParameter or interpolated overload — never string.Concat
var patients = await _db.Patients
    .FromSqlRaw(
        "SELECT * FROM patients WHERE ward_id = @wardId AND is_deleted = 0",
        new SqlParameter("@wardId", wardId.Value))
    .AsNoTracking()
    .ToListAsync(ct);

// Global query filters still apply unless IgnoreQueryFilters() is called
// You can chain LINQ after FromSqlRaw
var activeCount = await _db.Patients
    .FromSqlRaw("SELECT * FROM patients WHERE ward_id = @wardId",
        new SqlParameter("@wardId", wardId.Value))
    .Where(p => !p.IsDeleted)
    .CountAsync(ct);

FromSqlInterpolated — Safe Interpolation

C#
// FromSqlInterpolated: automatically parameterizes the interpolated values
// SAFE — each {variable} becomes a SqlParameter
var patients = await _db.Patients
    .FromSqlInterpolated(
        $"SELECT * FROM patients WHERE ward_id = {wardId.Value} AND is_deleted = 0")
    .AsNoTracking()
    .ToListAsync(ct);

// NEVER use FromSqlRaw with string interpolation or concatenation:
// BAD — SQL injection vulnerability:
var bad = await _db.Patients
    .FromSqlRaw($"SELECT * FROM patients WHERE mrn = '{mrnInput}'")
    .ToListAsync(ct);
// If mrnInput = "'; DROP TABLE patients; --" → data loss

SqlQuery for Arbitrary Projections (.NET 8+)

C#
// EF Core 8+: query for non-entity types using SqlQuery<T>
// No longer restricted to entity types mapped in the model

var summary = await _db.Database
    .SqlQuery<PatientWardSummaryDto>($@"
        SELECT
            w.ward_name        AS {nameof(PatientWardSummaryDto.WardName)},
            COUNT(p.id)        AS {nameof(PatientWardSummaryDto.PatientCount)},
            AVG(i.inr_value)   AS {nameof(PatientWardSummaryDto.AverageInr)}
        FROM wards w
        LEFT JOIN patients p  ON p.ward_id = w.id AND p.is_deleted = 0
        LEFT JOIN inr_results i ON i.patient_id = p.id AND i.recorded_at > {cutoff}
        WHERE w.id = {wardId.Value}
        GROUP BY w.ward_name")
    .ToListAsync(ct);

// Column names in SQL must match DTO property names (case-insensitive)

ExecuteSqlRaw — Non-Query Commands

C#
// Bulk update without loading entities
var affected = await _db.Database.ExecuteSqlRawAsync(
    "UPDATE prescriptions SET is_active = 0 WHERE patient_id = @patientId AND expiry_date < @now",
    new SqlParameter("@patientId", patientId.Value),
    new SqlParameter("@now", DateTime.UtcNow));

// Stored procedure call
await _db.Database.ExecuteSqlRawAsync(
    "EXEC sp_ArchiveDischargedPatients @wardId = @wardId, @beforeDate = @date",
    new SqlParameter("@wardId", wardId.Value),
    new SqlParameter("@date", cutoffDate));

// ExecuteSqlRaw runs outside the change tracker — EF Core doesn't know what changed.
// If you update rows that are tracked in the current context,
// the tracked entities are now stale. Either reload them or dispose the context.

Stored Procedures for Entity Results

C#
// Stored procedure that returns Patient-shaped rows
var patients = await _db.Patients
    .FromSqlRaw("EXEC sp_GetWardPatients @wardId",
        new SqlParameter("@wardId", wardId.Value))
    .AsNoTracking()
    .ToListAsync(ct);

// If stored procedure returns multiple result sets, use Dapper instead
// EF Core FromSqlRaw handles only the first result set

When to Drop to Dapper

C#
// Use Dapper when:
//   • Multiple result sets from one query
//   • Complex projections not matching any entity shape
//   • You want full control over mapping

// Dapper shares the same connection — works inside EF Core transaction
using var transaction = await _db.Database.BeginTransactionAsync(ct);

// EF operation
await _db.Prescriptions.AddAsync(prescription, ct);
await _db.SaveChangesAsync(ct);

// Dapper query on the same connection/transaction
var connection = _db.Database.GetDbConnection();
var summary    = await connection.QueryFirstOrDefaultAsync<WarfarinSummaryDto>(
    "SELECT * FROM v_warfarin_summary WHERE patient_id = @id",
    new { id = prescription.PatientId.Value },
    transaction.GetDbTransaction());

await transaction.CommitAsync(ct);

Production issue I've seen: A developer wrote FromSqlRaw($"SELECT * FROM patients WHERE mrn = '{request.Mrn}'") — direct string interpolation into raw SQL. In testing, it worked fine. In a security audit, the pen tester passed ' OR '1'='1 as the MRN and retrieved every patient record in the database. FromSqlInterpolated or explicit SqlParameter would have parameterized the value and prevented this. Always use FromSqlInterpolated or explicit SqlParameter — never concatenate user input into SQL strings.


Key Takeaway

Use FromSqlInterpolated() or explicit SqlParameter — never concatenate strings into raw SQL. Use SqlQuery<T>() (.NET 8+) to project arbitrary SQL to non-entity DTOs. Use ExecuteSqlRaw() for bulk operations that don't need entity tracking. Drop to Dapper for multi-result-set queries or complex projections. Raw SQL bypasses the change tracker — tracked entities become stale after bulk updates.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.