Dapper is fast and lightweight, but bulk operations can still be painful when you scale data volume.
Row-by-row inserts, updates, and deletes add latency and put unnecessary pressure on your database.
In today's blog post, we'll explore Dapper Plus, a high-performance library that brings bulk operations to Dapper and ADO.NET workflows without changing your mental model.
Dapper Plus
Dapper Plus is a commercial library that adds bulk insert, update, delete, merge, and synchronize to Dapper.
It executes optimized SQL directly in the database, bypassing any ORM tracking and keeping you in full control of the SQL lifecycle.
To get started with Dapper Plus, install the NuGet package. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:
Install-Package Z.Dapper.Plus
Once installed, map your entities and use extension methods directly on your IDbConnection.
For this blog post, we'll use a simple Product entity with a relationship to ProductReview:
public class Product
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public List<ProductReview> Reviews { get; set; }
}
public class ProductReview
{
public Guid Id { get; set; }
public Guid ProductId { get; set; }
public Product Product { get; set; }
public string Author { get; set; }
public string Content { get; set; }
public int Rating { get; set; }
public DateTime CreatedAt { get; set; }
}
Mapping
Dapper Plus relies on explicit mapping so the library knows how to save your entities. At minimum, you need to specify the table name, but you can customize exactly which properties to save and how.
Here is a minimal mapping setup that specifies table names:
DapperPlusManager.Entity<Product>().Table("Products");
DapperPlusManager.Entity<ProductReview>().Table("ProductReviews");
But mapping can do much more. You can map specific properties, constant values, computed values, and even database-side formulas. Here's a more advanced example:
DapperPlusManager.Entity<Product>()
.Table("Products")
.Identity(x => x.Id)
.Map(x => new { x.Name, x.Description, x.Price })
.MapValue(DateTime.UtcNow, "CreatedAt")
.MapWithOptions(x => x.ModifiedAt, options => {
options.FormulaInsert = "now()";
});
DapperPlusManager.Entity<ProductReview>()
.Table("ProductReviews")
.Identity(x => x.Id)
.Map(x => x.Product.Id, "ProductId")
.AutoMap();
In this example, for Product we map only specific properties, set a constant value for CreatedAt, and use a database formula for ModifiedAt. For ProductReview, we map ProductId from the parent and then call AutoMap to handle the remaining properties automatically.
NOTE: Once you start explicit mapping, Dapper Plus stops using auto-mapping logic unless you explicitly call AutoMap. This gives you full control over what gets saved.
BulkInsert
BulkInsert is the most common operation. Instead of looping inserts, it generates optimized SQL that inserts everything in a single roundtrip:
products.MapPost("/bulk-insert", (IDbConnection connection) =>
{
var items = ProductFactory.Generate(50).ToList();
connection.BulkInsert(items);
return Results.Ok(new { inserted = items.Count });
});
BulkInsert doesn't track entities or run through any ORM lifecycle. You stay explicit and in control.
BulkInsert Related Data
With Dapper Plus, you insert related data explicitly. That means you control order and keys, which is ideal when you already manage your SQL flow:
products.MapPost("/bulk-insert-related", (IDbConnection connection) =>
{
var items = ProductFactory.Generate(10).ToList();
connection.BulkInsert(items);
var reviews = items
.SelectMany(p => p.Reviews)
.ToList();
connection.BulkInsert(reviews);
return Results.Ok(new { inserted = items.Count, reviews = reviews.Count });
});
This keeps everything explicit: you insert Products, then their Reviews.
BulkUpdate
BulkUpdate generates optimized SQL that updates all records in a single database operation:
products.MapPut("/bulk-update", (IDbConnection connection, List<Guid> ids) =>
{
var items = connection.Query<Product>(
"SELECT * FROM Products WHERE Id IN @ids",
new { ids }).ToList();
foreach (var p in items)
{
p.Price += 10;
p.ModifiedAt = DateTime.UtcNow;
}
connection.BulkUpdate(items);
return Results.Ok(new { updated = items.Count });
});
It identifies entities by their primary key and updates all modified properties.
BulkDelete
BulkDelete deletes multiple entities efficiently without loading them into an ORM change tracker:
products.MapDelete("/bulk-delete", (IDbConnection connection, List<Guid> ids) =>
{
var items = ids
.Select(id => new Product { Id = id })
.ToList();
connection.BulkDelete(items);
return Results.Ok(new { deleted = items.Count });
});
BulkDelete identifies entities to delete by their primary key, so you only need to provide entities with their IDs populated.
BulkMerge
BulkMerge performs an upsert using the primary key to insert new entities and update existing ones in a single operation:
products.MapPost("/bulk-merge", (IDbConnection connection) =>
{
var items = ProductFactory.Generate(30).ToList();
connection.BulkMerge(items);
return Results.Ok(new { merged = items.Count });
});
BulkSynchronize
BulkSynchronize goes a step further than BulkMerge by also deleting entities that exist in the database but not in your collection, ensuring your database exactly matches your in-memory collection:
products.MapPost("/bulk-synchronize", (IDbConnection connection) =>
{
var items = ProductFactory.Generate(10).ToList();
connection.BulkSynchronize(items);
return Results.Ok(new { synchronized = items.Count });
});
Dapper Plus vs EF Extensions
Both Dapper Plus and EF Extensions are products from Z.BulkOperations and share the same bulk operation engine. The difference is how you interact with them.
Performance is the same. The difference is developer ergonomics and abstraction level. If you're already using EF Core, check out this blog post: Getting Started with Entity Framework Extensions.
Use Dapper Plus when you already write SQL, care about imports/exports or ETL, and want maximum control with minimum overhead.
Use EF Extensions when you already have an EF Core app and want bulk operations without rewriting architecture.
Conclusion
Dapper Plus delivers highly optimized bulk operations while keeping you in Dapper and ADO.NET land.
Bulk insert, update, delete, merge, and synchronize run directly in the database with minimal overhead.
Same performance as EF Extensions, different abstraction level.
Although it’s a commercial library, the speed and productivity gains often justify the investment.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
