Skip to content

Dapper Query Adapter

How do you directly control SQL in production? InMemoryQueryBase processes in-memory data with LINQ, but in actual services, SQL queries must be composed and executed directly against a database. DapperQueryBase<TEntity, TDto> is the common infrastructure for SQL-based Query Adapters. This chapter covers SQL generation patterns through SqlQueryBuilder without an actual DB.


After completing this chapter, you will be able to:

  1. Explain DapperQueryBase’s Template Method pattern and the items subclasses must implement
  2. Identify the SQL differences between Offset-based and Cursor-based pagination
  3. Apply SQL Injection prevention using AllowedSortColumns
  4. Understand the structural symmetry between InMemoryQueryBase and DapperQueryBase

DapperQueryBase Subclass Implementation Items

Section titled “DapperQueryBase Subclass Implementation Items”

If InMemoryQueryBase handled filtering/sorting/projection with C# code, DapperQueryBase performs the same roles with SQL. See what subclasses must implement.

Abstract MemberRoleExample
SelectSqlSELECT query bodySELECT p.id, p.name FROM products p
CountSqlCOUNT query bodySELECT COUNT(*) FROM products p
DefaultOrderByDefault ORDER BY clausep.name ASC
AllowedSortColumnsAllowed sort column mapping{ "Name": "p.name" }
BuildWhereClauseSpecification -> SQL WHEREWHERE p.stock > 0

Compare how the two pagination approaches learned in the previous chapter are expressed in SQL.

-- Offset-based
SELECT * FROM products WHERE stock > 0 ORDER BY name ASC LIMIT 10 OFFSET 20
-- Cursor-based
SELECT * FROM products WHERE stock > 0 AND id > @CursorValue ORDER BY id LIMIT 10

Offset uses a LIMIT/OFFSET combination, while Cursor specifies the starting point with a WHERE condition.

Maps client sort field names (e.g., “Name”) to actual SQL column names (e.g., “p.name”). Fields not in the mapping are rejected, preventing SQL Injection.

Both Query Adapters implement the same IQueryPort interface but differ in internal processing.

AspectInMemoryQueryBaseDapperQueryBase
Data sourceConcurrentDictionary (memory)IDbConnection (SQL DB)
FilteringSpecification.IsSatisfiedBy (C#)BuildWhereClause (SQL WHERE)
SortingSortSelector (C# function)AllowedSortColumns (SQL ORDER BY)
ProjectionLINQ Select (C#)SelectSql (SQL SELECT)
UsageTesting, prototypingProduction

Simplifies the SQL composition patterns that DapperQueryBase performs internally. The actual DapperQueryBase uses Dapper’s DynamicParameters and QueryMultipleAsync, but this chapter focuses on SQL string generation.

  • BuildSelectWithPagination: Offset-based SELECT query (LIMIT/OFFSET)
  • BuildSelectWithCursor: Cursor-based SELECT query (WHERE + LIMIT)
  • BuildCount: COUNT query
  • BuildOrderBy: ORDER BY clause with AllowedSortColumns mapping applied

ItemDescription
DapperQueryBaseCommon base for SQL-based Query Adapters
SelectSql / CountSqlSQL body declared by subclasses
BuildWhereClauseSpecification -> SQL WHERE conversion
AllowedSortColumnsClient field name -> SQL column name mapping (Injection prevention)
PaginationClauseLIMIT/OFFSET (supports DB dialects via override)

Q1: Why learn with SqlQueryBuilder instead of using DapperQueryBase directly?

Section titled “Q1: Why learn with SqlQueryBuilder instead of using DapperQueryBase directly?”

A: DapperQueryBase requires an actual DB connection (IDbConnection). This chapter focuses on learning core SQL generation pattern concepts without a DB. Production implementations inherit from DapperQueryBase.

Q2: How is Specification converted to SQL in BuildWhereClause?

Section titled “Q2: How is Specification converted to SQL in BuildWhereClause?”

A: Injecting DapperSpecTranslator handles the conversion automatically. Alternatively, subclasses can directly override BuildWhereClause to generate SQL by Specification type.

A: Override PaginationClause and CursorPaginationClause. The defaults are PostgreSQL/SQLite style (LIMIT/OFFSET), but they can be replaced with SQL Server’s OFFSET FETCH or TOP.


The Query-side patterns are complete. Now it’s time to integrate Command and Query into Usecases. How do you convert the FinT returned by Repository into an API response? In Part 4, we’ll explore how to compose Command and Query through the Usecase layer.

-> Chapter 1: Command Usecase