关于 dotnet-dapper-query-builder
面向 .NET 开发者的 Dapper 查询编写参考技能,提供查询处理器和数据访问模式的模板。内容涵盖多重映射、分页、CTE 及参数化查询等做法。
主要功能
- 01异步查询方法参考
- 02多重映射处理关联数据
- 03分页与筛选模板
- 04CTE 查询示例
- 05参数化与列别名规则
适用场景
- 01编写按 ID 获取记录的处理器
- 02构建带筛选的分页查询
- 03通过联接查询映射关联数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ ronnythedev/ dotnet-clean-architecture-skills --skill dotnet-dapper-query-builder安装后在 Claude Code 中输入/dotnet-dapper-query-builder确认已加载。
详细介绍
Dapper 查询构建器
概述
Dapper 提供轻量、高性能的数据访问:
- 原生 SQL - 完全掌控查询
- 多重映射 - 处理复杂联接
- 参数化查询 - 防止 SQL 注入
- 开销极低 - 性能接近 ADO.NET
快速参考
| 方法 | 用途 | 使用场景 |
|---|---|---|
QueryAsync<T> | 多行数据 | 列表、报表 |
QueryFirstOrDefaultAsync<T> | 单行数据 | 按 ID 获取 |
QueryMultipleAsync | 多个结果集 | 复杂数据 |
ExecuteAsync | 无返回结果 | 插入/更新/删除 |
ExecuteScalarAsync<T> | 单个值 | 计数、判断是否存在 |
模板:基本查询处理器
// 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;
}
}
模板:多重映射(一对多)
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;
}
}
模板:带筛选的分页查询
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 = new
{
SearchPattern = searchPattern,
request.OrganizationId,
request.IsActive,
Offset = offset,
request.PageSize
};
var totalCount = await connection.ExecuteScalarAsync<int>(countSql, parameters);
var items = await connection.QueryAsync<{Entity}Response>(dataSql, parameters);
return new PagedList<{Entity}Response>(
items.ToList(),
request.PageNumber,
request.PageSize,
totalCount);
}
}
模板:CTE(通用表表达式)
const string sql = """
WITH RankedItems AS (
SELECT
e.*,
ROW_NUMBER() OVER (PARTITION BY e.category_id ORDER BY e.score DESC) as rank
FROM entity e
WHERE e.organization_id = @OrganizationId
),
TopItems AS (
SELECT * FROM RankedItems WHERE rank <= 3
)
SELECT
ti.id AS Id,
ti.name AS Name,
ti.score AS Score,
ti.rank AS Rank,
c.name AS CategoryName
FROM TopItems ti
INNER JOIN category c ON ti.category_id = c.id
ORDER BY c.name, ti.rank
""";
模板:SQL 连接工厂
// src/{name}.application/Abstractions/Data/ISqlConnectionFactory.cs
using System.Data;
namespace {name}.application.abstractions.data;
public interface ISqlConnectionFactory
{
IDbConnection CreateConnection();
}
// src/{name}.infrastructure/Data/SqlConnectionFactory.cs
using System.Data;
using Npgsql;
namespace {name}.infrastructure.data;
internal sealed class SqlConnectionFactory : ISqlConnectionFactory
{
private readonly string _connectionString;
public SqlConnectionFactory(string connectionString)
{
_connectionString = connectionString;
}
public IDbConnection CreateConnection()
{
var connection = new NpgsqlConnection(_connectionString);
connection.Open();
return connection;
}
}
列映射(Snake Case → PascalCase)
SELECT
e.id AS Id, -- Maps to Id
e.first_name AS FirstName, -- Maps to FirstName
e.created_at AS CreatedAt, -- Maps to CreatedAt
e.organization_id AS OrganizationId -- Maps to OrganizationId
FROM entity e
关键规则
- 始终使用参数 - 切勿拼接用户输入
- 使用
using管理连接 - 正确释放资源 - 为列设置别名以匹配 DTO -
AS PropertyName - 联接使用多重映射 - 避免 N+1 查询
- 复杂逻辑使用 CTE - 比嵌套查询更易读
- 使用 OFFSET/FETCH 分页 - 标准 SQL 分页
- 不区分大小写时使用 ILIKE - PostgreSQL 专用
- 返回 DTO 而非实体 - 仅查询投影
相关技能
dotnet-cqrs-query-generator- 查询处理器结构dotnet-repository-pattern- 使用 EF Core 进行写入dotnet-clean-architecture- 应用层
评论
支持 Markdown