Imagine a form that only ever expects a ticket number, like 482. SQL injection is what happens when someone types something sneaky into that box instead of a number — a trick phrase that makes the system say "show me every ticket" instead of just ticket 482, because the system never checked that what it received was actually just a number.
SQL injection happens when user-controlled input gets inserted directly into a database query's text. In C#, string interpolation ($"...") reads exactly like ordinary, idiomatic string building — which is what makes it easy to accidentally use for a SqlCommand's query text.
In 2015, UK telecom TalkTalk suffered a breach affecting over 150,000 customers after attackers exploited a SQL injection flaw in a legacy web page inherited through a company acquisition. The UK's data protection regulator fined TalkTalk £400,000, describing the failure as preventable and basic.
Source: UK Information Commissioner's Office enforcement notice, 2016 — see References below.// userId comes straight from the request
var query =
$"SELECT * FROM Users WHERE Id = {userId}";
var cmd = new SqlCommand(query, connection);
var reader = cmd.ExecuteReader();
// value bound as a parameter
var cmd = new SqlCommand(
"SELECT * FROM Users WHERE Id = @id",
connection);
cmd.Parameters.AddWithValue("@id", userId);
var reader = cmd.ExecuteReader();
SqlCommand.Parameters sends the value to SQL Server separately from the compiled query text, so the value can never alter the query's structure. A $"..." interpolated string can't provide this — the value is already baked into the string before SqlCommand ever sees it.
$"WHERE Id = {userId}" and a hardcoded $"WHERE Id = 42" are visually indistinguishable at a glance — the danger is purely in what fills the interpolation, which is easy to miss in review.
EF Core's normal LINQ queries parameterize automatically. FromSqlRaw() built with string concatenation does not. FromSqlInterpolated() is a genuinely EF-specific safety feature — it accepts an interpolated string but auto-parameterizes each interpolated value instead of inlining it, so the safe pattern in EF looks almost identical to the unsafe one in plain ADO.NET.
connection.Query<User>("... WHERE Id = @Id", new { Id = userId }) is safe. Building the SQL string first and passing no parameters object is not, regardless of how the string was assembled.
FromSqlRaw exists precisely for cases needing raw SQL, and it's opt-in unsafe unless parameterized — EF doesn't prevent you from writing it.
Type inference is a real (separate) footgun with AddWithValue for performance reasons, but the parameter-binding safety property holds regardless — don't confuse the type-inference caveat with the injection protection, which is solid either way.
grep -rn 'new SqlCommand($"' --include="*.cs" .
grep -rn "FromSqlRaw(" --include="*.cs" . | grep '+\|\$"'
Yes — EF Core intercepts the interpolation at compile time and converts each interpolated value into a real bound parameter before the query runs. This is different from a plain C# interpolated string passed to SqlCommand, which stays a flat string.
Only when you pass a parameters object alongside a parameterized query string — building the SQL text yourself and passing no parameters object bypasses that entirely.