🚀 在 ABP 框架中优雅集成 Dapper:一站式封装事务、多结果集与分页查询 🎉
随着业务复杂度的提升和性能要求的不断攀升,虽然 EF Core 功能强大,但在某些场景下,基于 Dapper 的原生 SQL 查询依然不可或缺。本文将结合 ABP vNext,介绍如何封装一个通用且高效的 DapperHelper
,帮助你:
- 🔥 性能需求:在高并发或数据量大的场景下,EF Core 的 LINQ-to-Entities 生成 SQL 可能略显臃肿,Dapper 原生 SQL 性能更优
- 🔗 一致性封装:项目中零散地使用 Dapper 很容易导致连接、事务管理分散,难以统一维护
- 🔧 灵活性:需要同时支持单表 CRUD、复杂联表、分页、甚至存储过程等多种用例
借助 ABP 的依赖注入与 EF Core 的连接管理能力,我们可以优雅地将 Dapper 封装成一个可插拔、可扩展的组件。
📖 背景与动机
- 🔥 性能需求:在高并发或数据量大的场景下,EF Core 的 LINQ-to-Entities 生成 SQL 可能略显臃肿,Dapper 原生 SQL 性能更优
- 🔗 一致性封装:项目中零散地使用 Dapper 很容易导致连接、事务管理分散,难以统一维护
- 🔧 灵活性:需要同时支持单表 CRUD、复杂联表、分页、甚至存储过程等多种用例
借助 ABP 的依赖注入与 EF Core 的连接管理能力,我们可以优雅地将 Dapper 封装成一个可插拔、可扩展的组件。
📋 接口定义:IDapperHelper
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
namespace YourProject.Dapper
{
/// <summary>
/// Dapper 操作接口定义,封装常用数据库操作。
/// </summary>
public interface IDapperHelper
{
/// <summary>
/// 执行查询并返回多个实体对象。
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="sql">SQL 查询语句</param>
/// <param name="parameters">查询参数(可选)</param>
/// <param name="commandType">命令类型(可选)</param>
/// <param name="cancellationToken">取消令牌(可选)</param>
/// <returns>对象集合</returns>
Task<IEnumerable<T>> QueryAsync<T>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default
);
/// <summary>
/// 执行查询并返回首个对象,或默认值。
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="sql">SQL 查询语句</param>
/// <param name="parameters">查询参数(可选)</param>
/// <param name="commandType">命令类型(可选)</param>
/// <param name="cancellationToken">取消令牌(可选)</param>
/// <returns>首个对象或 null</returns>
Task<T?> QueryFirstOrDefaultAsync<T>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default
);
/// <summary>
/// 执行非查询语句(如 INSERT、UPDATE、DELETE)。
/// </summary>
/// <param name="sql">SQL 执行语句</param>
/// <param name="parameters">参数对象(可选)</param>
/// <param name="commandType">命令类型(可选)</param>
/// <param name="cancellationToken">取消令牌(可选)</param>
/// <returns>受影响的行数</returns>
Task<int> ExecuteAsync(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default
);
/// <summary>
/// 显式开启事务并执行操作。
/// </summary>
/// <param name="action">事务操作委托,提供连接与事务对象</param>
/// <param name="cancellationToken">取消令牌(可选)</param>
/// <returns>异步任务</returns>
Task ExecuteInTransactionAsync(
Func<IDbConnection, IDbTransaction, CancellationToken, Task> action,
CancellationToken cancellationToken = default
);
/// <summary>
/// 查询并读取两个结果集。
/// </summary>
/// <typeparam name="T1">第一个结果集类型</typeparam>
/// <typeparam name="T2">第二个结果集类型</typeparam>
/// <param name="sql">包含多个结果集的 SQL 语句</param>
/// <param name="parameters">查询参数(可选)</param>
/// <param name="commandType">命令类型(可选)</param>
/// <param name="cancellationToken">取消令牌(可选)</param>
/// <returns>包含两个集合的元组</returns>
Task<(IEnumerable<T1>, IEnumerable<T2>)> QueryMultipleAsync<T1, T2>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default
);
/// <summary>
/// 查询并读取三个结果集。
/// </summary>
/// <typeparam name="T1">第一个结果集类型</typeparam>
/// <typeparam name="T2">第二个结果集类型</typeparam>
/// <typeparam name="T3">第三个结果集类型</typeparam>
/// <param name="sql">包含多个结果集的 SQL 语句</param>
/// <param name="parameters">查询参数(可选)</param>
/// <param name="commandType">命令类型(可选)</param>
/// <param name="cancellationToken">取消令牌(可选)</param>
/// <returns>包含三个集合的元组</returns>
Task<(IEnumerable<T1>, IEnumerable<T2>, IEnumerable<T3>)> QueryMultipleAsync<T1, T2, T3>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default
);
/// <summary>
/// 执行分页查询。
/// </summary>
/// <typeparam name="T">返回类型</typeparam>
/// <param name="baseSql">基础 SQL(不含分页)</param>
/// <param name="pageIndex">页码(从 1 开始)</param>
/// <param name="pageSize">每页记录数</param>
/// <param name="parameters">查询参数(可选)</param>
/// <param name="commandType">命令类型(可选)</param>
/// <param name="cancellationToken">取消令牌(可选)</param>
/// <returns>分页结果集合</returns>
Task<IEnumerable<T>> QueryPagedAsync<T>(
string baseSql,
int pageIndex,
int pageSize,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default
);
}
}
- ✨ 统一接口:所有常见操作一应俱全
- 🚀 异步友好:基于
async/await
,可传递CancellationToken
- 🧩 可扩展:支持多结果集与分页
🔨 核心实现:DapperHelper
using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
namespace YourProject.EntityFrameworkCore.Dapper
{
/// <summary>
/// Dapper 操作辅助类,支持事务控制、日志记录、多结果集与分页查询。
/// </summary>
public class DapperHelper : IDapperHelper, ITransientDependency
{
private readonly IDbContextProvider<YourProjectDbContext> _dbContextProvider;
private readonly ILogger<DapperHelper> _logger;
public DapperHelper(
IDbContextProvider<YourProjectDbContext> dbContextProvider,
ILogger<DapperHelper> logger)
{
_dbContextProvider = dbContextProvider;
_logger = logger;
}
private async Task<(IDbConnection, IDbTransaction?)> GetConnectionAndTransactionAsync()
{
var dbContext = _dbContextProvider.GetDbContext();
var connection = dbContext.Database.GetDbConnection();
if (connection.State != ConnectionState.Open)
{
await connection.OpenAsync();
}
var transaction = dbContext.Database.CurrentTransaction?.GetDbTransaction();
return (connection, transaction);
}
public async Task<IEnumerable<T>> QueryAsync<T>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default)
{
var (conn, tran) = await GetConnectionAndTransactionAsync();
_logger.LogDebug("QueryAsync SQL: {Sql}, Params: {@Params}", sql, parameters);
return await conn.QueryAsync<T>(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
}
public async Task<T?> QueryFirstOrDefaultAsync<T>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default)
{
var (conn, tran) = await GetConnectionAndTransactionAsync();
_logger.LogDebug("QueryFirstOrDefaultAsync SQL: {Sql}, Params: {@Params}", sql, parameters);
return await conn.QueryFirstOrDefaultAsync<T>(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
}
public async Task<int> ExecuteAsync(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default)
{
var (conn, tran) = await GetConnectionAndTransactionAsync();
_logger.LogDebug("ExecuteAsync SQL: {Sql}, Params: {@Params}", sql, parameters);
return await conn.ExecuteAsync(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
}
public async Task ExecuteInTransactionAsync(
Func<IDbConnection, IDbTransaction, CancellationToken, Task> action,
CancellationToken cancellationToken = default)
{
var dbContext = await _dbContextProvider.GetDbContextAsync();
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async execCt =>
{
await using var efTransaction = await dbContext.Database.BeginTransactionAsync(execCt);
var connection = dbContext.Database.GetDbConnection();
if (connection.State != ConnectionState.Open)
{
await connection.OpenAsync(execCt);
}
var dbTransaction = efTransaction.GetDbTransaction();
try
{
_logger.LogDebug("Begin Transaction");
await action(connection, dbTransaction, execCt);
await efTransaction.CommitAsync(execCt);
_logger.LogDebug("Transaction Committed");
}
catch (Exception ex)
{
_logger.LogError(ex, "Transaction Rolled Back");
await efTransaction.RollbackAsync(execCt);
throw;
}
}, cancellationToken);
}
public async Task<(IEnumerable<T1>, IEnumerable<T2>)> QueryMultipleAsync<T1, T2>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default)
{
var (conn, tran) = await GetConnectionAndTransactionAsync();
_logger.LogDebug("QueryMultiple<T1, T2> SQL: {Sql}, Params: {@Params}", sql, parameters);
using var multi = await conn.QueryMultipleAsync(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
var first = await multi.ReadAsync<T1>();
var second = await multi.ReadAsync<T2>();
return (first, second);
}
public async Task<(IEnumerable<T1>, IEnumerable<T2>, IEnumerable<T3>)> QueryMultipleAsync<T1, T2, T3>(
string sql,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default)
{
var (conn, tran) = await GetConnectionAndTransactionAsync();
_logger.LogDebug("QueryMultiple<T1, T2, T3> SQL: {Sql}, Params: {@Params}", sql, parameters);
using var multi = await conn.QueryMultipleAsync(new CommandDefinition(sql, parameters, tran, cancellationToken: cancellationToken, commandType: commandType));
var first = await multi.ReadAsync<T1>();
var second = await multi.ReadAsync<T2>();
var third = await multi.ReadAsync<T3>();
return (first, second, third);
}
public async Task<IEnumerable<T>> QueryPagedAsync<T>(
string baseSql,
int pageIndex,
int pageSize,
object? parameters = null,
CommandType commandType = CommandType.Text,
CancellationToken cancellationToken = default)
{
var dynamicParams = new DynamicParameters(parameters);
dynamicParams.Add("Offset", (pageIndex - 1) * pageSize);
dynamicParams.Add("PageSize", pageSize);
string pagedSql;
// === 请取消注释你使用的数据库类型 ===
// -- SQL Server / PostgreSQL(默认) --
pagedSql = $"{baseSql} OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
// -- MySQL --
// pagedSql = $"{baseSql} LIMIT @PageSize OFFSET @Offset";
_logger.LogDebug("QueryPaged SQL: {Sql}, Params: {@Params}", pagedSql, dynamicParams);
var (conn, tran) = await GetConnectionAndTransactionAsync();
return await conn.QueryAsync<T>(new CommandDefinition(pagedSql, dynamicParams, tran, cancellationToken: cancellationToken, commandType: commandType));
}
}
}
🚀 实现机制详解
📐 功能支持矩阵
功能名称 | 是否支持 | 备注说明 |
---|---|---|
查询(单/多行) | ✅ | 使用 QueryAsync / QueryFirstOrDefaultAsync 实现 |
执行 SQL | ✅ | 兼容 INSERT/UPDATE/DELETE |
事务控制 | ✅ | 使用 EF Core 的事务封装 |
多结果集 | ✅ | 支持 QueryMultipleAsync |
分页查询 | ✅ | 跨数据库 SQL 模板切换 |
日志记录 | ✅ | 使用 ILogger 输出完整 SQL 与参数 |
依赖注入 | ✅ | 与 ABP 生命周期完美集成 |
🛠️ 方法清单表
方法名称 | 返回类型 | 典型用途 |
---|---|---|
QueryAsync<T> | IEnumerable<T> | 返回多行数据 |
QueryFirstOrDefaultAsync<T> | T? | 查询单条数据 |
ExecuteAsync | int | 执行写操作 |
QueryMultipleAsync<T1, T2> | (IEnumerable<T1>, IEnumerable<T2>) | 多结果集支持 |
ExecuteInTransactionAsync | Task | 控制事务性操作 |
QueryPagedAsync<T> | IEnumerable<T> | 分页查询支持 |
🔌 获取连接与事务:统一资源管理
GetConnectionAndTransactionAsync
方法负责从 DbContext
中获取数据库连接与当前事务,确保 Dapper 操作与 EF Core 操作共享事务上下文,避免数据一致性问题。
private async Task<(IDbConnection, IDbTransaction?)> GetConnectionAndTransactionAsync()
- 如果连接未打开,则自动打开
- 支持从 EF 的
Database.CurrentTransaction
中提取事务对象
🔍 查询方法:简化数据读取
封装了常见的查询方法,包括:
QueryAsync<T>
:通用多行查询QueryFirstOrDefaultAsync<T>
:只取首行,适合详情或单值查询QueryMultipleAsync<T1, T2, T3>
:支持多结果集
每个方法均支持:
- 可选参数(匿名对象)
- 命令类型控制(
Text
或StoredProcedure
) - 日志记录 SQL 与参数
_logger.LogDebug("QueryAsync SQL: {Sql}, Params: {@Params}", sql, parameters);
🧾 命令执行:安全调用数据库写操作
ExecuteAsync
提供插入、更新、删除等操作,封装事务处理逻辑,保证安全性与一致性。
public async Task<int> ExecuteAsync(...)
- 返回受影响的行数
- 适合用于业务层判断操作是否成功
🔁 事务封装:优雅处理重试与失败回滚
通过 EF Core 的 ExecutionStrategy
实现的 ExecuteInTransactionAsync
支持自动重试机制,是数据库死锁或临时错误的可靠应对方式。
await strategy.ExecuteAsync(async execCt =>
{
using var efTransaction = await dbContext.Database.BeginTransactionAsync(execCt);
...
});
- 自动打开连接、开启事务
- 自动提交或捕获异常并回滚
- 支持日志记录事务状态(开始、提交、回滚)
📦 多结果集:高效处理复杂查询
使用 QueryMultipleAsync
提供最多三组数据集的读取能力,适用于多表联合查询、主从数据结构等场景。
using var multi = await conn.QueryMultipleAsync(...);
var first = await multi.ReadAsync<T1>();
- 避免多次连接调用
- 提高性能与一致性
📄 分页查询:数据库兼容的分页封装
QueryPagedAsync
提供跨数据库(PostgreSQL、SQL Server、MySQL)兼容的分页支持。
pagedSql = $"{baseSql} OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
// MySQL 示例(可替换)
pagedSql = $"{baseSql} LIMIT @PageSize OFFSET @Offset";
- 自动构造分页参数
- 支持自定义 SQL 拼接
- 日志记录执行语句,便于调试
🗂️ 数据库分页语法对比
数据库类型 | 分页语法样例 |
---|---|
SQL Server / PostgreSQL | OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY |
MySQL | LIMIT @PageSize OFFSET @Offset |
Oracle | 需使用 ROWNUM 或子查询嵌套 |
🧪 使用示例
🔍 查询活跃用户
public async Task<List<UserDto>> GetActiveUsersAsync(CancellationToken cancellationToken)
{
var sql = "SELECT Id, UserName, Email FROM AbpUsers WHERE IsActive = @Active";
return (await _dapper.QueryAsync<UserDto>(sql, new { Active = true }, cancellationToken: cancellationToken)).AsList();
}
🔍 分页查询
public async Task<PagedResultDto<UserDto>> GetPagedUsersAsync(int pageIndex, int pageSize, CancellationToken cancellationToken)
{
var count = await _dapper.QueryFirstOrDefaultAsync<int>("SELECT COUNT(*) FROM AbpUsers", cancellationToken: cancellationToken);
var dataSql = "SELECT Id, UserName, Email FROM AbpUsers ORDER BY CreationTime DESC";
var items = await _dapper.QueryPagedAsync<UserDto>(dataSql, pageIndex, pageSize, cancellationToken: cancellationToken);
return new PagedResultDto<UserDto>
{
TotalCount = count,
Items = items.AsList()
};
}
🔍 多结果集查询
public async Task<UserWithRolesResult> GetUsersAndRolesAsync(CancellationToken cancellationToken)
{
var sql = @"
SELECT Id, UserName, Email FROM AbpUsers;
SELECT Id, Name FROM AbpRoles;";
var (users, roles) = await _dapper.QueryMultipleAsync<UserDto, RoleDto>(sql, cancellationToken: cancellationToken);
return new UserWithRolesResult { Users = users.AsList(), Roles = roles.AsList() };
}
✏️ 更新并记录日志
public async Task UpdateUserAndLogAsync(Guid userId, string newEmail, CancellationToken cancellationToken)
{
try
{
await _dapper.ExecuteInTransactionAsync(async (conn, tran, ct) =>
{
await conn.ExecuteAsync(new CommandDefinition(
"UPDATE AbpUsers SET Email = @Email WHERE Id = @Id",
new { Email = newEmail, Id = userId }, tran, cancellationToken: ct));
await conn.ExecuteAsync(new CommandDefinition(
"INSERT INTO OperationLogs (UserId, Action, Timestamp) VALUES (@UserId, 'UpdateEmail', NOW())",
new { UserId = userId }, tran, cancellationToken: ct));
}, cancellationToken);
_logger.LogInformation("Successfully updated user {UserId}'s email", userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update user {UserId}'s email", userId);
throw;
}
}
🧱完整代码
public class UserQueryAppService : ApplicationService
{
private readonly IDapperHelper _dapper;
private readonly ILogger<UserQueryAppService> _logger;
public UserQueryAppService(IDapperHelper dapper, ILogger<UserQueryAppService> logger)
{
_dapper = dapper;
_logger = logger;
}
public async Task<List<UserDto>> GetActiveUsersAsync(CancellationToken cancellationToken)
{
var sql = "SELECT Id, UserName, Email FROM AbpUsers WHERE IsActive = @Active";
return (await _dapper.QueryAsync<UserDto>(sql, new { Active = true }, cancellationToken: cancellationToken)).AsList();
}
public async Task<PagedResultDto<UserDto>> GetPagedUsersAsync(int pageIndex, int pageSize, CancellationToken cancellationToken)
{
var count = await _dapper.QueryFirstOrDefaultAsync<int>("SELECT COUNT(*) FROM AbpUsers", cancellationToken: cancellationToken);
var dataSql = "SELECT Id, UserName, Email FROM AbpUsers ORDER BY CreationTime DESC";
var items = await _dapper.QueryPagedAsync<UserDto>(dataSql, pageIndex, pageSize, cancellationToken: cancellationToken);
return new PagedResultDto<UserDto>
{
TotalCount = count,
Items = items.AsList()
};
}
public async Task<UserWithRolesResult> GetUsersAndRolesAsync(CancellationToken cancellationToken)
{
var sql = @"
SELECT Id, UserName, Email FROM AbpUsers;
SELECT Id, Name FROM AbpRoles;";
var (users, roles) = await _dapper.QueryMultipleAsync<UserDto, RoleDto>(sql, cancellationToken: cancellationToken);
return new UserWithRolesResult { Users = users.AsList(), Roles = roles.AsList() };
}
public async Task UpdateUserAndLogAsync(Guid userId, string newEmail, CancellationToken cancellationToken)
{
try
{
await _dapper.ExecuteInTransactionAsync(async (conn, tran, ct) =>
{
await conn.ExecuteAsync(new CommandDefinition(
"UPDATE AbpUsers SET Email = @Email WHERE Id = @Id",
new { Email = newEmail, Id = userId }, tran, cancellationToken: ct));
await conn.ExecuteAsync(new CommandDefinition(
"INSERT INTO OperationLogs (UserId, Action, Timestamp) VALUES (@UserId, 'UpdateEmail', NOW())",
new { UserId = userId }, tran, cancellationToken: ct));
}, cancellationToken);
_logger.LogInformation("Successfully updated user {UserId}'s email", userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update user {UserId}'s email", userId);
throw;
}
}
}
✅ 常见问题解答(FAQ)
Q1: 如何支持 MySQL 分页?
- 将分页 SQL 修改为
LIMIT @PageSize OFFSET @Offset
,并注释掉 SQL Server 的写法。
Q2: 是否支持 IN
查询?
- Dapper 原生支持列表参数,例如:
SELECT * FROM Users WHERE Id IN @Ids
Q3: 如何单元测试 DapperHelper?
- 可以注入 Mock 的
IDbConnection
或 EF 的 InMemory Provider。
Q4: 是否支持 Oracle?
- 支持,但分页 SQL 语法需适配
ROWNUM
。
🎯 总结与扩展
- 🚫 零侵入:既可走 EF Core 的 UoW,也可随时手动开启 Dapper 事务
- ♻️ 代码复用:所有模块只需注入
IDapperHelper
即可调用 - 🔍 日志审计:内置
ILogger
,方便定位慢 SQL 与参数 - 🌱 易于扩展:可基于此基础,继续实现存储过程、批量插入、分库查询等高级功能