Last Updated on February 18, 2026 by Aram
When it comes to building CRUD API, you do not have to choose between control and productivity.
You can have both.
That can be simply achieved with combining Dapper and Dapper Plus.
Dapper Plus is your ultimate library and companion for write operations, it particularly shines when you want to perform bulk operations on thousands or millions of records.
In a previous tutorial, we learned about Dapper Plus and its key feature, Bulk Extensions. We got introduced to the different extension methods that would allow you to achieve blazing-fast bulk operations.
Special thanks for ZZZ Projects for sponsoring this article, and for delivering and continuously maintaining Dapper Plus along with a list of other useful libraries.
Dapper Plus provides you with over 100 options to control how commands are sent and how data is stored in your database provider.
In this tutorial, you will learn how to build a complete Blogging API using:
- ASP.NET Core 10 Web API
- SQL Server
- Dapper for queries
- Dapper Plus for inserts, updates, and deletes
The result is fast, clean, and scalable.
Follow me with this step-by-step guide where we will exploring the amazing powers of Dapper for read operations, and Dapper Plus for write and bulk operations.
Why Combine Dapper and Dapper Plus?
You already know this:
Dapper is extremely fast with full SQL control, which makes it perfect for reads
But for writes it becomes repetitive.
You end up writing Insert, update, delete statements along with parameter mappings.
Now if you add Dapper Plus, you would get bulk operations (insert, update, delete, merge), without having to write any SQL.
Note that you can still use Dapper for writes, but you would be sending insert command, one by one, which would be a terrible solution at scale.
So combining both Dapper and Dapper Plus is the most efficient way to build a comprehensive solution for highly effective reads and writes on massive scale, where Dapper would be used for queries, and Dapper Plus for commands.
So your structure would benefit from the clear separation, leading to a cleaner architecture.
Prepare the Database
I usually prefer to prepare my database separately, without letting any tool or library do that on behalf of me, just to make sure that I am in full control on my database and its tables structure.
So make sure you have a local SQL Server Express Database Installed and connected to your local machine.

Then go and create a new database with name “BloggingDb”
After that create a new table, Posts
use BloggingDb
CREATE TABLE Posts
(
Id INT IDENTITY(1,1) PRIMARY KEY,
Title NVARCHAR(200) NOT NULL,
Content NVARCHAR(MAX) NOT NULL,
Author NVARCHAR(150) NOT NULL,
CreatedAt DATETIME2 NOT NULL
);
Create new ASP.NET Core Web API Project
Open your favorite editor, currently I am using Visual Studio 2026, it is the latest and great version of Visual Studio.
Select create a new project and choose ASP.NET Core Web API Template.
Dapper and Dapper Plus Nuget Packages
For this tutorial we will need to import both Dapper and Dapper Plus Nuget Packages:

Also to connect to SQL Server Database, you will need to important the related SQL Client Nuget package:

Program.cs
You will need to inject the related dependenceis in your project along with the configuration of Dapper Plus
builder.Services.AddScoped(); builder.Services.AddScoped (); builder.Services.AddScoped (); DapperPlusManager.Entity () .Table("dbo.Posts") .Identity(p => p.Id); var app = builder.Build();
Interfaces
In this tutorial we are separating the reads from the writes, so this separation is better handled with abstractions, just to make sure you have different interface for each use case.
This also conforms to SOLID’s interface segregation principal.
IDbConnectionFactory
using Microsoft.Data.SqlClient;
namespace DapperAndDapperPlus.Interfaces
{
public interface IDbConnectionFactory
{
SqlConnection CreateConnection();
}
}
IPostCommandService
using DapperAndDapperPlus.Models;
namespace DapperAndDapperPlus.Interfaces
{
public interface IPostCommandService
{
Task CreateAsync(Post post);
Task UpdateAsync(Post post);
Task DeleteAsync(Post post);
Task BulkImportAsync(IEnumerable posts);
}
}
IPostQueryService
using DapperAndDapperPlus.Models;
namespace DapperAndDapperPlus.Interfaces
{
public interface IPostQueryService
{
Task> GetAllAsync();
Task GetByIdAsync(int id);
}
}
Factories
The Factory will be used as a wrapper solution for the creation of the connection.
Dapper is responsible for:
• Executing SQL
• Mapping results
The factory does not wrap Dapper. It just supplies the connection.
DbConnectionFactory
using DapperAndDapperPlus.Interfaces;
using Microsoft.Data.SqlClient;
namespace DapperAndDapperPlus.Factories
{
public class DbConnectionFactory(IConfiguration configuration) : IDbConnectionFactory
{
private readonly string _connectionString =
configuration.GetConnectionString("BloggingDbConnection")!;
public SqlConnection CreateConnection()
=> new(_connectionString);
}
}
Appsettings.json
Since we are using the configuration to pull the connection string, it is now important to define that section in your appsettings.json file.
So typically for starter, your appsettings.json file should look like this:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"BloggingDbConnection": "Server=localhost\\SQLEXPRESS;Database=BloggingDb;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=Yes"
},
"AllowedHosts": "*"
}
Services
We will have implementations for both IPostQueryService and IPostCommandService.
PostQueryService
using DapperAndDapperPlus.Interfaces;
using DapperAndDapperPlus.Models;
using Dapper;
namespace DapperAndDapperPlus.Services
{
public class PostQueryService(IDbConnectionFactory factory) : IPostQueryService
{
public async Task> GetAllAsync()
{
using var connection = factory.CreateConnection();
const string sql = """
SELECT Id, Title, Content, Author, CreatedAt
FROM Posts
ORDER BY CreatedAt DESC
""";
return await connection.QueryAsync(sql);
}
public async Task GetByIdAsync(int id)
{
using var connection = factory.CreateConnection();
const string sql = """
SELECT *
FROM Posts
WHERE Id = @Id
""";
return await connection.QueryFirstOrDefaultAsync(
sql,
new { Id = id });
}
}
}
PostCommandService
using DapperAndDapperPlus.Interfaces;
using DapperAndDapperPlus.Models;
using Z.Dapper.Plus;
namespace DapperAndDapperPlus.Services
{
public class PostCommandService(IDbConnectionFactory factory) : IPostCommandService
{
public async Task CreateAsync(Post post)
{
using var connection = factory.CreateConnection();
await connection.SingleInsertAsync(post);
return post.Id;
}
public async Task UpdateAsync(Post post)
{
using var connection = factory.CreateConnection();
await connection.SingleUpdateAsync(post);
}
public async Task DeleteAsync(Post post)
{
using var connection = factory.CreateConnection();
var post = new Post { Id = id };
await connection.SingleDeleteAsync(post);
}
public async Task BulkImportAsync(IEnumerable posts)
{
using var connection = factory.CreateConnection();
await connection.BulkInsertAsync(posts);
}
}
}
Controllers
And here is the code of the controller that will expose the endpoint related to the posts API.
using DapperAndDapperPlus.Interfaces;
using DapperAndDapperPlus.Models;
using Microsoft.AspNetCore.Mvc;
namespace DapperAndDapperPlus.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class PostsController(
IPostQueryService queryService,
IPostCommandService commandService) : ControllerBase
{
[HttpGet]
public async Task GetAll()
=> Ok(await queryService.GetAllAsync());
[HttpGet("{id}")]
public async Task Get(int id)
{
var post = await queryService.GetByIdAsync(id);
return post is null ? NotFound() : Ok(post);
}
[HttpPost]
public async Task Create(Post post)
{
post.CreatedAt = DateTime.UtcNow;
var id = await commandService.CreateAsync(post);
return CreatedAtAction(nameof(Get), new { id }, post);
}
[HttpPut("{id}")]
public async Task Update(int id, Post post)
{
if (post.Id != 0 && id != post.Id)
return BadRequest();
var savedPost = await queryService.GetByIdAsync(id);
if (savedPost is null)
{
return BadRequest();
}
savedPost.Author = post.Author;
savedPost.Content = post.Content;
savedPost.Title = post.Title;
await commandService.UpdateAsync(savedPost);
return NoContent();
}
[HttpDelete("{id}")]
public async Task Delete(int id)
{
var savedPost = await queryService.GetByIdAsync(id);
if (savedPost is null)
{
return BadRequest();
}
await commandService.DeleteAsync(savedPost);
return NoContent();
}
[HttpPost("bulk")]
public async Task BulkImport(List posts)
{
foreach (var post in posts)
post.CreatedAt = DateTime.UtcNow;
await commandService.BulkImportAsync(posts);
return Ok();
}
}
}
As you can see we are injecting both the commandService and the queryService to perform the different CRUD operations on the Posts through the same API endpoint with different methods.
Your project structure should look like the below screenshot:

Testing with Postman
Now that we have prepared our APIs and connected them to the database through both Dapper and Dapper Plus, where the DbConnectionFactory will be managing the connections between the reads and writes.
Let’s run the application, and then open Postman to start testing our solution.
The base URL used here is https://localhost:7016, the port will be different at your side, so be aware of that once you run your project.
Save Post

Get All Posts

Get Post By ID

Update Post


Delete Post


Bulk Import


Why This Architecture Works for Many Platforms, like Blogging?
These platforms share common requirements, including:
- They are read-heavy
- They can be occasionally bulk-import heavy
- And they are query-driven
Such cases would require utilizing both Dapper and Dapper Plus for the ultimate performance and usability.
Dapper gives you full control for:
- Search
- Filtering
- Pagination
- Custom projections
Dapper Plus gives you:
- Clean write operations
- Efficient bulk imports
- Less boilerplate
Final Thoughts
You do not need to treat ORMs as all-or-nothing decisions.
For a blogging platform:
Use Dapper when reading posts.
Use Dapper Plus when writing posts.
That balance keeps your API:
- Fast
- Clean
- Maintainable
This tutorial only touched the surface of these amazing libraries, there are still a lot more to explore, particularly when it comes to bulk operations, where Dapper Plus would shine and stand out as the best and the fastest library for this purpose.
You can try it yourself and explore the amazing features of Dapper Plus, there is a free trial that you can download and extend at the end of each month for few months before you decide to purchase the license.
Visit Dapper Plus Official Site to Learn More.
Bonus
Enjoy the brilliant baroque tunes of Arcangelo Corelli with his magnificent Concerti Grossi, Opus 6.
