mcpField
D
dotnet-dapper-query-builder

dotnet-dapper-query-builder

github.com/ronnythedev数据与数据库7767 次安装15 周前更新
代码仓库

关于 dotnet-dapper-query-builder

面向 .NET 开发者的 Dapper 查询编写参考技能,提供查询处理器和数据访问模式的模板。内容涵盖多重映射、分页、CTE 及参数化查询等做法。

主要功能

  1. 01异步查询方法参考
  2. 02多重映射处理关联数据
  3. 03分页与筛选模板
  4. 04CTE 查询示例
  5. 05参数化与列别名规则

适用场景

  1. 01编写按 ID 获取记录的处理器
  2. 02构建带筛选的分页查询
  3. 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

关键规则

  1. 始终使用参数 - 切勿拼接用户输入
  2. 使用 using 管理连接 - 正确释放资源
  3. 为列设置别名以匹配 DTO - AS PropertyName
  4. 联接使用多重映射 - 避免 N+1 查询
  5. 复杂逻辑使用 CTE - 比嵌套查询更易读
  6. 使用 OFFSET/FETCH 分页 - 标准 SQL 分页
  7. 不区分大小写时使用 ILIKE - PostgreSQL 专用
  8. 返回 DTO 而非实体 - 仅查询投影

相关技能

  • dotnet-cqrs-query-generator - 查询处理器结构
  • dotnet-repository-pattern - 使用 EF Core 进行写入
  • dotnet-clean-architecture - 应用层

评论

支持 Markdown