Skip to content

Dapper Generated SQL

AmirHosseinMp02 edited this page Aug 1, 2026 · 1 revision

Dapper: Generated SQL

Exactly what SqlQueryCompiler emits, statement by statement. Everything here is engine-independent structure; the parts that vary are supplied by the dialect and are marked.


The six statements

SqlQueryCompiler has six public compile methods. Which ones run depends on the query shape.

MethodPurposeUsed by
CompileSchemaProbediscover the target's columns and typesonce per object, ever
CompileRowCountMeta.Total for a flat queryevery flat query
CompileRowsthe page of rowsevery flat query
CompileGroupCountMeta.Total for a grouped queryevery grouped query
CompileGroupKeysthe page of outermost group keysevery grouped query
CompileGroupRowsevery row under those keysevery grouped query

Each returns:

publicsealedrecordCompiledSql(stringText,IReadOnlyDictionary<string,object?>Parameters);

Text contains only placeholders — never inlined literals. Parameters is keyed without the dialect's prefix (p0, not @p0).


1. Schema probe

SELECT*FROM<source>WHERE1=0

Returns no rows. The reader's FieldCount, GetName(i) and GetFieldType(i) give the column whitelist and the CLR type of each column. Cached per provider | schema | name | type for the process lifetime.

If a driver cannot describe a column's type, the name is still recorded and the type is left unknown — in which case no coercion is applied to values compared against it.

2. Flat: row count

SELECTCOUNT(*) FROM<source>[ WHERE<predicate>]

No ORDER BY, no paging. This is Meta.Total.Rows; Meta.Total.Pages is ceil(rows / size) computed in the application.

3. Flat: rows

SELECT<projection>FROM<source>[ WHERE<predicate>][ ORDER BY<terms>] <paging>
  • <projection> is * when SelectColumns is empty or names nothing real, otherwise the quoted, comma-separated list of the recognised columns.
  • <paging> is always emitted.
  • When there is no usable sort column and the dialect requires an ORDER BY for paging (SQL Server), a placeholder is emitted first: ORDER BY (SELECT NULL).

4. Grouped: group count

SELECTCOUNT(*) FROM (SELECT DISTINCT<key>FROM<source>[ WHERE<predicate>]) qf_groups

The alias is written without AS — Oracle rejects AS before a table alias, and every other engine accepts the bare form, so one spelling works everywhere.

This is the count of distinct outermost keys, which becomes Meta.Total.Rows for a grouped result.

5. Grouped: group keys

SELECT DISTINCT<key>FROM<source>[ WHERE<predicate>] ORDER BY<key term><paging>

Ordered by the outermost GroupByDescriptor.SortOrder, with its null-ordering clause. This is the statement paging actually slices.

6. Grouped: rows

SELECT<projection + grouping columns>FROM<source>WHERE (<predicate>) AND<key predicate>[ ORDER BY<sort terms>]

No paging clause — every row under the paged keys is fetched, because a node's Count is the number of leaf rows beneath it and that cannot be known from a partial set. See Grouping and Hierarchies.

When there is no filter, the WHERE is just the key predicate.

The key predicate

IN never matches NULL, so nulls are handled explicitly:

Keys on the pageEmitted
all non-null<key> IN (@p0, @p1, …)
some null(<key> IN (@p0, …) OR <key> IS NULL)
only null<key> IS NULL
none1 = 0

The FROM source

DapperObjectTypeSQL Server / PostgreSQLMySQL / SQLiteOracle
Auto, Table, View[schema].[name]`name` (schema only if given)"NAME"
TVF[schema].[fn](@p0, @p1)not supportedTABLE("FN"(:p0, :p1))
SPnot compiled — see below

TVF arguments are positional, taken in the order of the Parameters dictionary, and each becomes a parameter.

SP is never composed into a SELECT. Calling CompileSchemaProbe or any other compile method with an SP target throws InvalidOperationException — the executor routes procedures down a separate path that calls them and filters in memory. See Dapper Provider.


The WHERE clause

Built group by group. Only conditions that are usableand name a whitelisted column contribute.

(<condition> <AND|OR> <condition> …) one group
NOT (<condition> …) a negated group (AndNot / OrNot)
<group> <AND|OR> <group> … groups joined by criteria.Logic

A group producing no fragments is skipped entirely — it does not emit () or 1=1. If no group produces anything, no WHERE clause is emitted at all.

Per-operator output

col is the quoted column, @pN a parameter reference in the dialect's form.

OperatorEmittedNotes
Equals, value setcol = @p0
Equals, value nullcol IS NULLno parameter
NotEquals, value setcol <> @p0
NotEquals, value nullcol IS NOT NULLno parameter
LessThancol < @p0
GreaterThancol > @p0
LessThanOrEqualTocol <= @p0
GreaterThanOrEqualTocol >= @p0
Betweencol BETWEEN @p0 AND @p1two parameters
Containscol LIKE @p0 ESCAPE '\'pattern %value%
NotContainscol NOT LIKE @p0 ESCAPE '\'pattern %value%
StartsWithcol LIKE @p0 ESCAPE '\'pattern value%
EndsWithcol LIKE @p0 ESCAPE '\'pattern %value

The ESCAPE clause is the dialect's; MySQL emits ESCAPE '\\' because it processes backslash escapes inside string literals.

The pattern is built after escaping the caller's value, so wildcards inside it match literally. A search for 50% becomes the parameter %50\%% with ESCAPE '\'.

Values are coerced to the column's real type before binding — see Query Semantics.


ORDER BY

One term per usable sort column, in list order:

<quoted column> <ASC|DESC><null ordering>

<null ordering> is the dialect's, and includes a leading space or is empty:

EngineAscendingDescending
PostgreSQL, Oracle NULLS FIRST NULLS LAST
SQL Server, MySQL, SQLite(empty — default already matches)(empty)

The standard is nulls first ascending, nulls last descending, everywhere. See Query Semantics.

Paging clause

EngineEmitted
SQL Server, OracleOFFSET <n> ROWS FETCH NEXT <m> ROWS ONLY
PostgreSQL, MySQL, SQLiteLIMIT <m> OFFSET <n>

with m = Size > 0 ? Size : 12, n = (max(Number,1) - 1) * m.

The offset and size are inlined as integers, not parameters. They are computed from validated integers, never from caller text, and inlining lets the optimizer see the actual window.


Parameters

  • Named p0, p1, … in the order the compiler encounters them.
  • Numbered per statement — the count query and the row query each start at p0, and identical filters produce identical names in both.
  • Rendered with the dialect's prefix: @p0 everywhere, :p0 on Oracle.
  • Never derived from column names, which avoids reserved-word collisions such as Oracle's ORA-01745.
  • Bound by name; BindByName is set reflectively on drivers that expose it.

Worked examples

Model:

publicclassUser{publicintUserId{get;set;}publicstringFirstName{get;set;}publicstringCountry{get;set;}publicstring?Department{get;set;}publicdecimalScore{get;set;}publicboolIsActive{get;set;}}

Flat, filtered, sorted, paged, projected

vardq=DapperQueryBuilder.Where(newQueryCriteria([newConditionGroup([newCondition("Country",ConditionOperator.Equals,"Germany"),newCondition("Score",ConditionOperator.GreaterThan,50)])])).Select("UserId","FirstName","Score").Sort(newSortDescriptor("Score",SortOrder.Descending)).Page(20,2).ForObject("Users","dbo").Build();

SQL Server

-- countSELECTCOUNT(*) FROM [dbo].[Users] WHERE ([Country] = @p0 AND [Score] > @p1)
-- rowsSELECT [UserId], [FirstName], [Score] FROM [dbo].[Users]
WHERE ([Country] = @p0 AND [Score] > @p1)
ORDER BY [Score] DESC
OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLY

PostgreSQL

SELECT"UserId", "FirstName", "Score"FROM"public"."Users"WHERE ("Country"= @p0 AND"Score"> @p1)
ORDER BY"Score"DESC NULLS LAST
LIMIT20 OFFSET 20

MySQL

SELECT`UserId`, `FirstName`, `Score`FROM`Users`WHERE (`Country`= @p0 AND`Score`> @p1)
ORDER BY`Score`DESCLIMIT20 OFFSET 20

Oracle

SELECT"UserId", "FirstName", "Score"FROM"Users"WHERE ("Country"= :p0 AND"Score"> :p1)
ORDER BY"Score"DESC NULLS LAST
OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLY

Parameters in all four: p0 = "Germany", p1 = 50m — coerced to decimal because that is the column's type, even if the client sent "50".

Nested logic with a negated group

{ "criteria": { "logic": 0, "groups": [
{ "logic": 1, "conditions": [
{ "columnName": "Country", "operator": 0, "value": "Germany" },
{ "columnName": "Country", "operator": 0, "value": "Canada" } ] },
{ "logic": 2, "conditions": [
{ "columnName": "Department", "operator": 0, "value": "HR" } ] } ] } }
WHERE ([Country] = @p0 OR [Country] = @p1) AND NOT ([Department] = @p2)

Text search with a wildcard in the value

newCondition("FirstName",ConditionOperator.Contains,"50%")
WHERE ([FirstName] LIKE @p0 ESCAPE '\')-- p0 = "%50\%%"

Null checks

newCondition("Department",ConditionOperator.Equals,null)// → [Department] IS NULL
new Condition("Department",ConditionOperator.NotEquals,null)// → [Department] IS NOT NULL

Neither emits a parameter.

A grouped query

vardq=DapperQueryBuilder.Where(newQueryCriteria([newConditionGroup([newCondition("IsActive",ConditionOperator.Equals,true)])])).Select("UserId","FirstName","Score").Sort(newSortDescriptor("Score",SortOrder.Descending)).GroupBy(newGroupByDescriptor("Country"),newGroupByDescriptor("Department")).Page(5,1).ForObject("Users","dbo").Build();

Three statements, on SQL Server:

-- 1. how many distinct countries matchSELECTCOUNT(*) FROM (
SELECT DISTINCT [Country] FROM [dbo].[Users] WHERE ([IsActive] = @p0)
) qf_groups
-- 2. the first five of themSELECT DISTINCT [Country] FROM [dbo].[Users] WHERE ([IsActive] = @p0)
ORDER BY [Country] ASC
OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
-- 3. every row in those five countries.-- Country and Department are appended to the projection because the tree is rebuilt from them.SELECT [UserId], [FirstName], [Score], [Country], [Department] FROM [dbo].[Users]
WHERE ([IsActive] = @p0) AND [Country] IN (@p1, @p2, @p3, @p4, @p5)
ORDER BY [Score] DESC

The hierarchy is then assembled from statement 3's rows by HierarchyBuilder.

A table-valued function

.ForObject("tvf_GetUsersByTenant","dbo",DapperObjectType.TVF,newDictionary<string,object?>{["TenantId"]=1})
-- SQL ServerSELECT*FROM [dbo].[tvf_GetUsersByTenant](@p0) WHERE-- PostgreSQLSELECT*FROM"public"."tvf_GetUsersByTenant"(@p0) WHERE-- OracleSELECT*FROM TABLE("TVF_GETUSERSBYTENANT"(:p0)) WHERE

Seeing the SQL yourself

The compiler is public, so you can inspect what a query produces without a database:

usingPepperX.QueryForge.Dapper.Compiler;usingPepperX.QueryForge.Dapper.Dialects;varcompiler=newSqlQueryCompiler(newPostgreSqlDialect());// Supply the whitelist yourself instead of probing a database.varcolumns=newColumnWhitelist(newDictionary<string,Type?>{["UserId"]=typeof(int),["Country"]=typeof(string),["Score"]=typeof(decimal)});varsql=compiler.CompileRows(dapperQuery,columns);Console.WriteLine(sql.Text);foreach(var(name,value)insql.Parameters)Console.WriteLine($" {name} = {value}");

This is exactly how the repository's own dialect and compiler tests work — see Testing.

For the EF Core provider the equivalent is ToQueryString():

db.Users.ApplyQuery(query).ToQueryString();

Clone this wiki locally