Dapper是一个轻量级的ORM(对象关系映射)框架,专为.NET设计。它通过扩展IDbConnection接口,使开发者能够方便地执行SQL查询,并将查询结果映射到对象模型中。类似于ADO.NET
Dapper基础:CURD

查询数据
Dapper使用Query方法执行SQL查询并返回结果集。
匿名查询
只用了Query(sql),而不是Query<T>()
cs
//查询前10条,返回第一条
string sql = "SELECT TOP 10 * FROM OrderDetails";
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
var orderDetail = connection.Query(sql).FirstOrDefault();
FiddleHelper.WriteTable(orderDetail);
}
强类型查询
使用Query<T>()
cs
public class Student{
public int Id { get; set; }
public string Email { get; set; }
}
using (IDbConnection db = new SqlConnection(connectionString))
{
string sql = "SELECT Id, Email FROMFROM Students WHERE Email = @Email";
var students = db.Query<Student>(sql,new { Email = "test@xxx.com" }).ToList();
}
查询多映射
Invoice(发票)和InvoiceDetail(发票细节)
cs
// 发票主表实体
public class Invoice
{
public int ID { get; set; } // 主键
public string InvoiceNo { get; set; } // 发票编号
public DateTime InvoiceDate { get; set; } // 发票日期
// 其他发票字段...
// 关键修正:一对多关联 → 用集合存储多条明细(原代码是单个对象,错误)
public List<InvoiceDetail> InvoiceDetails { get; set; } = new List<InvoiceDetail>(); // 初始化避免空引用
// 必须重写 Equals 和 GetHashCode(用于 Distinct() 去重,基于主键 InvoiceID)
public override bool Equals(object obj)
{
return obj is Invoice invoice && InvoiceID == invoice.InvoiceID;
}
public override int GetHashCode()
{
return HashCode.Combine(InvoiceID);
}
}
// 发票明细表实体
public class InvoiceDetail
{
public int DetailID { get; set; } // 明细主键(关键!原代码可能缺少,导致 splitOn 映射错位)
public int InvoiceID { get; set; } // 外键(关联发票表)
public string ProductName { get; set; } // 产品名称
public decimal Quantity { get; set; } // 数量
public decimal UnitPrice { get; set; } // 单价
// 其他明细字段...
}
cs
string sql
= "SELECT * FROM Invoice AS A INNER JOIN InvoiceDetail AS B ON A.InvoiceID = B.InvoiceID;";
using (var connection = new SqlConnection(Connstr))
{
connection.Open();
//Dapper 映射:T1=Invoice(主表),T2=InvoiceDetail(从表),TReturn=Invoice
var invoices = connection.Query<Invoice, InvoiceDetail, Invoice>(
sql,
(invoice, invoiceDetail) =>
{
invoice.InvoiceDetail = invoiceDetail;
return invoice;
},
splitOn: "DetailID")
.Distinct()
.ToList();
}
splitOn: "InvoiceID"用于分隔查询字段,赋值,从这个把查询出来的字段,左边的赋值给主表,右边的赋值给从表,例如
cs
splitOn:"Aid"
A.id A.name A.password | B.Aid B.bname B.bpasswrod .
最好不要select *,而是select 具体字段。
如果分割线选的不对可能出错例如,
cs
SELECT
A.Id, A.InvoiceNo, A.InvoiceDate,
B.DetailId, -- 新增字段,放在 B.InvoiceId 前面
B.InvoiceId, B.ProductName...
此时 splitOn: "InvoiceId" 会找第一个 InvoiceId,主表没有DetailId这个属性,映射直接报错。
查询多映射(1对多)
cs
string sql = "SELECT TOP 10 * FROM Orders AS A INNER JOIN OrderDetails AS B ON A.OrderID = B.OrderID;";
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
var orderDictionary = new Dictionary<int, Order>();
var list = connection.Query<Order, OrderDetail, Order>(
sql,
(order, orderDetail) =>
{
Order orderEntry;
if (!orderDictionary.TryGetValue(order.OrderID, out orderEntry))
{
orderEntry = order;
orderEntry.OrderDetails = new List<OrderDetail>();
orderDictionary.Add(orderEntry.OrderID, orderEntry);
}
orderEntry.OrderDetails.Add(orderDetail);
return orderEntry;
},
splitOn: "OrderID")
.Distinct()
.ToList();
Console.WriteLine(list.Count);
FiddleHelper.WriteTable(list);
FiddleHelper.WriteTable(list.First().OrderDetails);
}
返回的[0x123,0x123,0x134].Distinct()==[0x123,0x134].
查询单个项
QueryFirst
QuerySingle
QueryFirstOrDefault,
QuerySingleOrDefault
查询到无值,一个值,多个值的结果:

他们也支持QueryFirst(匿名),QueryFirst<T>(强类型)两种方式。
批处理 SQL 返回多个结果
QueryMultiple
以下示例演示如何在一次往返数据库 中执行两个 SQL 语句。在第一个查询中,我们从表中获取所有发票,在第二个查询中,我们从表中获取所有发票项目
cs
string sql = "SELECT * FROM Invoices WHERE InvoiceID = @InvoiceID; SELECT * FROM InvoiceItems WHERE InvoiceID = @InvoiceID;";
using (var connection = My.ConnectionFactory())
{
connection.Open();
using (var multi = connection.QueryMultiple(sql, new {InvoiceID = 1}))
{
var invoice = multi.Read<Invoice>().First();
var invoiceItems = multi.Read<InvoiceItem>().ToList();
}
}
一次查询两个多结果的结果集
cs
{
List<Order> orders = new List<Order>();
List<OrderDetail> allDetails = new List<OrderDetail>();
// 需求:查询所有状态为"已付款"的订单 + 对应的所有明细(两个都是多结果集)
string sql = @"
-- 结果集1:多结果(所有已付款的订单)
SELECT OrderID, OrderNo, OrderDate, Status
FROM Orders
WHERE Status = @Status; -- 条件:已付款
-- 结果集2:多结果(这些订单对应的所有明细)
SELECT DetailID, OrderID, ProductName, Quantity, UnitPrice
FROM OrderDetails
WHERE OrderID IN (SELECT OrderID FROM Orders WHERE Status = @Status); -- 关联主表条件
";
try
{
using (var connection = CreateConnection())
{
await connection.OpenAsync();
// 执行多结果集查询(两个结果集都是多数据)
using (var multi = await connection.QueryMultipleAsync(sql, new { Status = "已付款" }))
{
// 1. 读取第一个多结果集:所有已付款的订单(转为列表)
orders = (await multi.ReadAsync<Order>()).ToList();
// 2. 读取第二个多结果集:所有关联的明细(转为列表)
allDetails = (await multi.ReadAsync<OrderDetail>()).ToList();
}
}
// 3. 核心:在内存中关联主表和从表(按 OrderID 分组匹配)
// 先将明细按 OrderID 分组(key=OrderID,value=该订单的所有明细)
var detailsGrouped = allDetails.GroupBy(d => d.OrderID).ToDictionary(g => g.Key, g => g.ToList());
// 遍历每个订单,将对应的明细赋值给 OrderDetails 集合
foreach (var order in orders)
{
// 若该订单有对应的明细,赋值;否则保持空集合(初始化时已 new List<>)
if (detailsGrouped.TryGetValue(order.OrderID, out var orderDetails))
{
order.OrderDetails = orderDetails;
}
}
// 输出结果验证
Console.WriteLine($"已付款订单总数:{orders.Count}");
foreach (var order in orders)
{
Console.WriteLine($"订单编号:{order.OrderNo},明细数量:{order.OrderDetails.Count}");
foreach (var detail in order.OrderDetails)
{
Console.WriteLine($" - 商品:{detail.ProductName},数量:{detail.Quantity}");
}
}
}
catch (SqlException ex)
{
Console.WriteLine($"数据库错误:{ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"系统错误:{ex.Message}");
}
}
QueryUnbufferedAsync()
异步流式查询,防止一次性读入大数据占据内存过大,该函数可以一条一条,将内存占用从1G减到1K。底层应该使用了数据库游标
支持匿名,强类型,一对一,一对多
cs
public class OrderDetail
{
public int OrderDetailID { get; set; }
public int OrderID { get; set; }
public int ProductID { get; set; }
public int Quantity { get; set; }
}
string sql = "SELECT TOP 10 * FROM OrderDetails";
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
await foreach (var orderDetail in connection.QueryUnbufferedAsync<OrderDetail>(sql))
{
Console.WriteLine($"{orderDetail.OrderDetailID} - {orderDetail.Quantity}");
}
}
一对一映射
cs
string sql = "SELECT * FROM Invoice AS A INNER JOIN InvoiceDetail AS B ON A.InvoiceID = B.InvoiceID;";
using (var connection = My.ConnectionFactory())
{
var invoices = new List<Invoice>();
await foreach (var invoice in connection.QueryUnbufferedAsync<Invoice, InvoiceDetail, Invoice>(
sql,
(inv, detail) =>
{
inv.InvoiceDetail = detail;
return inv;
},
splitOn: "InvoiceID"))
{
invoices.Add(invoice);
}
}
Execute
它可以执行一次或多次命令并返回受影响的行数。此方法通常用于执行:
存储过程
INSERT 语句
UPDATE 语句
DELETE 语句
执行存储过程
cs
var sql = "usp_UpdateTable";
using (var connection = new SqlConnection(connectionString))
{
var rowsAffected = connection.Execute(sql, new { id = 1, value1 = "ABC", value2 = "DEF" }, commandType: CommandType.StoredProcedure);
}
多次执行存储过程
cs
string sql = "Invoice_Insert";
using (var connection = My.ConnectionFactory())
{
var affectedRows = connection.Execute(sql,
new[]
{
new {Kind = InvoiceKind.WebInvoice, Code = "Many_Insert_1"},
new {Kind = InvoiceKind.WebInvoice, Code = "Many_Insert_2"},
new {Kind = InvoiceKind.StoreInvoice, Code = "Many_Insert_3"}
},
commandType: CommandType.StoredProcedure
);
My.Result.Show(affectedRows);
}
在上面的示例中,名为"Invoice_Insert"的存储过程将被调用三次。
插入
多条记录插入
cs
string sql = "INSERT INTO Customers (CustomerName) Values (@CustomerName);";
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
connection.Open();
var affectedRows = connection.Execute(sql,
new[]
{
new {CustomerName = "John"},
new {CustomerName = "Andy"},
new {CustomerName = "Allan"}
}
);
Console.WriteLine(affectedRows);
更新
在下面的示例中,它将更新两条等于"1"和"4"的记录
cs
string sql = "UPDATE Categories SET Description = @Description WHERE CategoryID = @CategoryID;";
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
var affectedRows = connection.Execute(sql,
new[]
{
new {CategoryID = 1, Description = "Soft drinks, coffees, teas, beers, mixed drinks, and ales"},
new {CategoryID = 4, Description = "Cheeses and butters etc."}
}
);
Console.WriteLine(affectedRows);
删除
cs
string sql = "DELETE FROM OrderDetails WHERE OrderDetailID = @OrderDetailID";
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
var affectedRows = connection.Execute(sql,
new[]
{
new {OrderDetailID = 1},
new {OrderDetailID = 2},
new {OrderDetailID = 3}
}
);
Console.WriteLine(affectedRows);
ExecuteReader
cs
using(var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServer()))
{
var reader = connection.ExecuteReader("SELECT * FROM Customers WHERE CreatedDate > @CreatedDate;", new { createdDate} );
DataTable table = new DataTable();
table.Load(reader);
FiddleHelper.WriteTable(table);
}
ExcuteScalar
它允许您执行 SQL 语句或存储过程,并在结果集中第一行的第一列返回标量值。
如果结果集包含多个列或行,则它仅采用第一行的第一列,所有其他值将被忽略。
如果结果集为空,它将返回引用。Null
与 or 等聚合函数一起使用非常有用。Count(*),Sum()
cs
//得到客户总数
using(var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServer()))
{
int rowCount = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM Customers");
Console.WriteLine(rowCount);
}
//得到客户id为1的人的名字
using(var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServer()))
{
var name = connection.ExecuteScalar<string>("SELECT Name FROM Customers WHERE CustomerID = @CustomerID;", new { CustomerID = 1});
Console.WriteLine(name);
}
多类型结果
cs
string sql = "SELECT * FROM Invoice;";
using (var connection = My.ConnectionFactory())
{
connection.Open();
var invoices = new List<Invoice>();
using (var reader = connection.ExecuteReader(sql))
{
var storeInvoiceParser = reader.GetRowParser<StoreInvoice>();得到转化器
var webInvoiceParser = reader.GetRowParser<WebInvoice>();
while (reader.Read())
{
Invoice invoice;
switch ((InvoiceKind) reader.GetInt32(reader.GetOrdinal("Kind")))int转成枚举
{
case InvoiceKind.StoreInvoice:
invoice = storeInvoiceParser(reader);转化
break;
case InvoiceKind.WebInvoice:
invoice = webInvoiceParser(reader);
break;
default:
throw new Exception(ExceptionMessage.GeneralException);
}
invoices.Add(invoice);
}
}
}
Dapper缓冲,就是提前ToList()
事务
cs
string sql = "INSERT INTO Customers (CustomerName) Values (@CustomerName);";
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
try
{
var affectedRows = connection.Execute(sql, new { CustomerName = "Mark" }, transaction: transaction);
transaction.Commit(); // ✅ Commit when everything is fine
Console.WriteLine(affectedRows);
}
catch
{
transaction.Rollback(); // ✅ Rollback if something goes wrong
throw;
}
}
}
Dapper Plus
PM> Install-Package Z.Dapper.Plus
1实体映射配置(DapperPlusManager)
cs
// 配置 Supplier 实体:映射到数据库表 Suppliers,主键是 SupplierID(Identity 表示"身份字段",通常是自增主键)
DapperPlusManager.Entity<Supplier>().Table("Suppliers").Identity(x => x.SupplierID);
目录
[批处理 SQL 返回多个结果](#批处理 SQL 返回多个结果)
[Dapper Plus](#Dapper Plus)
cs
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
// 核心链式操作:批量插入供应商 → 关联外键 → 批量插入产品
connection.BulkInsert(suppliers) // 第一步:批量插入所有供应商
.ThenForEach(x => x.Products.ForEach(p => p.SupplierID = x.SupplierID)) // 第二步:给产品设置外键(关联供应商ID)
.ThenBulkInsert(x => x.Products); // 第三步:批量插入所有产品
}
插入一对一
cs
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;
DapperPlusManager.Entity<Supplier>().Table("Suppliers").Identity(x => x.SupplierID);
DapperPlusManager.Entity<Product>().Table("Products").Identity(x => x.ProductID);
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
connection.BulkInsert(suppliers).ThenForEach(x => x.Product.SupplierID = x.SupplierID).ThenBulkInsert(x => x.Product);
}
一对多
cs
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;
DapperPlusManager.Entity<Supplier>().Table("Suppliers").Identity(x => x.SupplierID);
DapperPlusManager.Entity<Product>().Table("Products").Identity(x => x.ProductID);
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
connection.BulkInsert(suppliers).ThenForEach(x => x.Products.ForEach(y => y.SupplierID = x.SupplierID)).ThenBulkInsert(x => x.Products);
}
实体具有标识属性,但您希望强制它插入特定值
- 场景 1:数据库表主键是「非自增列」(如 UUID、业务自定义主键),需要手动指定
CustomerID值插入; - 场景 2:数据库表主键是「自增列」,但需要批量导入历史数据(历史数据已有固定主键值,不能重新生成);
- 场景 3:数据同步(从其他系统同步客户数据,主键值需保持一致,不能由当前数据库生成)。
cs
底层原理(以 SQL Server 为例)
Dapper Plus 执行时的底层 SQL 逻辑:
sql
-- 1. 开启手动插入标识列(允许手动传入 CustomerID)
SET IDENTITY_INSERT Customers ON;
-- 2. 批量插入数据(包含 CustomerID 字段)
BULK INSERT Customers (CustomerID, CustomerName, Email, CreateTime)
FROM ...; -- 数据来源(Dapper Plus 封装的内存数据)
-- 3. 关闭手动插入标识列(恢复默认行为)
SET IDENTITY_INSERT Customers OFF;
用法:
connection.UseBulkOptions(options => options.InsertKeepIdentity = true).BulkInsert(Entity);
例子:
cs
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;
DapperPlusManager.Entity<Customer>().Table("Customers");
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
var validCustomers = customers.Where(c => c.CustomerID > 0).ToList(); // 过滤无效主键
connection.UseBulkOptions(options => options.InsertKeepIdentity = true).BulkInsert(validCustomers);
}
字段映射必须正确(避免主键值插入失败)
cs
DapperPlusManager.Entity<Customer>()
.Table("Customers")
.Identity(x => x.CustID) // 实体主键
.Map(x => x.CustID, "CustomerID"); // 映射到表主键字段
.Map(x => x.CustomerName, "CustomerName")
.Map(x => x.Email, "Email")
.Map(x => x.CreateTime, "CreateTime");也可以用[Column] 特性(数据注解)
直接在实体类的属性上标记数据库字段名,无需在 DapperPlusManager 中通过 .Map() 显式配置
cs
数据库主键需支持手动插入
坑 1:若数据库表主键是「自增列」且未开启 IDENTITY_INSERT(Dapper Plus 会自动处理,但需确保当前用户有该权限),会抛 "不能为标识列插入显式值" 的错误;
坑 2:若实体中的 CustomerID 值重复(如两个客户的 CustomerID 都是 1001),插入时会抛 "主键约束冲突" 的错误;
解决:
确保实体中的 CustomerID 唯一(批量插入前校验去重);
确保数据库用户有 ALTER TABLE 或 IDENTITY_INSERT 权限(否则无法切换标识列开关)。
插入而不返回标识值
意思就是数据库生成的主键id回填到你实体类中。
配置 options.AutoMapOutputDirection = false,可以提升性能。
cs
// @nuget: Z.Dapper.Plus
using Z.Dapper.Plus;
DapperPlusManager.Entity<Customer>().Table("Customers");
using (var connection = new SqlConnection(FiddleHelper.GetConnectionStringSqlServerW3Schools()))
{
connection.UseBulkOptions(options => options.AutoMapOutputDirection = false).BulkInsert(customers);
FiddleHelper.WriteTable("1 - Customers (from list)", customers);
}