dotnet-dapper-query-builder
Generates optimized read queries using Dapper. Includes multi-mapping for joins, pagination, dynamic filtering, CTEs, and best practices for high-performance data access.
git clone --depth 1 https://github.com/ronnythedev/dotnet-clean-architecture-skills /tmp/dotnet-dapper-query-builder && cp -r /tmp/dotnet-dapper-query-builder/skills/19-dotnet-dapper-query-builder ~/.claude/skills/dotnet-dapper-query-builderSKILL.md
# Dapper Query Builder
## Overview
Dapper provides lightweight, high-performance data access:
- **Raw SQL** - Full control over queries
- **Multi-mapping** - Handle complex joins
- **Parameterized queries** - SQL injection protection
- **Minimal overhead** - Near ADO.NET performance
## Quick Reference
| Method | Purpose | Use Case |
|--------|---------|----------|
| `QueryAsync<T>` | Multiple rows | Lists, reports |
| `QueryFirstOrDefaultAsync<T>` | Single row | Get by ID |
| `QueryMultipleAsync` | Multiple result sets | Complex data |
| `ExecuteAsync` | No results | Insert/Update/Delete |
| `ExecuteScalarAsync<T>` | Single value | Count, exists |
---
## Template: Basic Query Handler
```csharp
// src/{name}.application/{Feature}/Get{Entity}ById/Get{Entity}ByIdQueryHandler.cs
using System.Data;
using Dapper;
using {name}.application.abstractions.data;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
namespace {name}.application.{feature}.Get{Entity}ById;
internal sealed class Get{Entity}ByIdQueryHandler
: IQueryHandler<Get{Entity}ByIdQuery, {Entity}Response>
{
private readonly ISqlConnectionFactory _sqlConnectionFactory;
public Get{Entity}ByIdQueryHandler(ISqlConnectionFactory sqlConnectionFactory)
{
_sqlConnectionFactory = sqlConnectionFactory;
}
public async Task<Result<{Entity}Response>> Handle(
Get{Entity}ByIdQuery request,
CancellationToken cancellationToken)
{
using IDbConnection connection = _sqlConnectionFactory.CreateConnection();
const string sql = """
SELECT
e.id AS Id,
e.name AS Name,
e.description AS Description,
e.is_active AS IsActive,
e.created_at AS CreatedAt,
o.id AS OrganizationId,
o.name AS OrganizationName
FROM entity e
INNER JOIN organization o ON e.organization_id = o.id
WHERE e.id = @Id
""";
var entity = await connection.QueryFirstOrDefaultAsync<{Entity}Response>(
sql,
new { request.Id });
return entity is null
? Result.Failure<{Entity}Response>({Entity}Errors.NotFound)
: entity;
}
}
```
---
## Template: Multi-Mapping (One-to-Many)
```csharp
internal sealed class Get{Entity}WithDetailsQueryHandler
: IQueryHandler<Get{Entity}WithDetailsQuery, {Entity}DetailResponse>
{
private readonly ISqlConnectionFactory _sqlConnectionFactory;
public async Task<Result<{Entity}DetailResponse>> Handle(
Get{Entity}WithDetailsQuery request,
CancellationToken cancellationToken)
{
using IDbConnection connection = _sqlConnectionFactory.CreateConnection();
const string sql = """
SELECT
e.id AS Id,
e.name AS Name,
c.id AS ChildId,
c.name AS ChildName,
c.sort_order AS SortOrder
FROM entity e
LEFT JOIN child c ON c.entity_id = e.id
WHERE e.id = @Id
ORDER BY c.sort_order
""";
var entityDict = new Dictionary<Guid, {Entity}DetailResponse>();
await connection.QueryAsync<{Entity}DetailResponse, ChildResponse, {Entity}DetailResponse>(
sql,
(entity, child) =>
{
if (!entityDict.TryGetValue(entity.Id, out var existingEntity))
{
existingEntity = entity;
existingEntity.Children = new List<ChildResponse>();
entityDict.Add(entity.Id, existingEntity);
}
if (child is not null)
{
existingEntity.Children.Add(child);
}
return existingEntity;
},
new { request.Id },
splitOn: "ChildId");
var result = entityDict.Values.FirstOrDefault();
return result is null
? Result.Failure<{Entity}DetailResponse>({Entity}Errors.NotFound)
: result;
}
}
```
---
## Template: Paginated Query with Filtering
```csharp
internal sealed class Search{Entities}QueryHandler
: IQueryHandler<Search{Entities}Query, PagedList<{Entity}Response>>
{
private readonly ISqlConnectionFactory _sqlConnectionFactory;
public async Task<Result<PagedList<{Entity}Response>>> Handle(
Search{Entities}Query request,
CancellationToken cancellationToken)
{
using IDbConnection connection = _sqlConnectionFactory.CreateConnection();
var offset = (request.PageNumber - 1) * request.PageSize;
var searchPattern = request.SearchTerm is not null
? $"%{request.SearchTerm}%"
: null;
// Build dynamic WHERE clause
var whereConditions = new List<string> { "1 = 1" };
if (searchPattern is not null)
whereConditions.Add("(e.name ILIKE @SearchPattern OR e.description ILIKE @SearchPattern)");
if (request.OrganizationId.HasValue)
whereConditions.Add("e.organization_id = @OrganizationId");
if (request.IsActive.HasValue)
whereConditions.Add("e.is_active = @IsActive");
var whereClause = string.Join(" AND ", whereConditions);
var countSql = $"""
SELECT COUNT(*)
FROM entity e
WHERE {whereClause}
""";
var dataSql = $"""
SELECT
e.id AS Id,
e.name AS Name,
e.description AS Description,
e.is_active AS IsActive,
e.created_at AS CreatedAt
FROM entity e
WHERE {whereClause}
ORDER BY e.created_at DESC
OFFSET @Offset ROWS
FETCH NEXT @PageSize ROWS ONLY
""";
var parameters = newScaffolds a complete .NET solution following Clean Architecture principles with proper layer separation (API, Application, Domain, Infrastructure). Creates project structure, dependency injection setup, and cross-cutting concerns configuration.
Generates CQRS Commands with Handlers, Validators, and Request DTOs following Clean Architecture patterns. Commands represent actions that modify state and return Result types for proper error handling.
Generates CQRS Queries with Handlers and Response DTOs for read operations. Uses Dapper for optimized read queries, bypassing the domain model for better performance.
Generates Domain Entities following DDD principles with factory methods, private setters, domain events, and proper encapsulation. Supports aggregate roots, child entities, and value objects.
Generates Repository interfaces and implementations following the Repository pattern. Provides data access abstraction for aggregate roots with EF Core implementations.
Generates Entity Framework Core configurations using Fluent API. Maps domain entities to database tables with proper relationships, constraints, and conventions.
Generates RESTful API Controllers with proper routing, versioning, authorization, and MediatR integration. Follows REST conventions and Clean Architecture patterns.
Generates Minimal API endpoints following Microsoft's recommended approach. Creates fast, testable HTTP APIs with minimal code using MapGet/MapPost/MapPut/MapDelete. Preferred over controller-based APIs for new projects.