DEV Community

Cover image for Transaction with Dapper in ASP.NET Core API: A Beginner’s Guide
Ravi Vishwakarma
Ravi Vishwakarma

Posted on

Transaction with Dapper in ASP.NET Core API: A Beginner’s Guide

When developing an ASP.NET Core Web API, we often need to perform multiple database operations as part of a single business operation.

For example, suppose we are creating an order. We may need to:

  1. Insert the order.
  2. Insert order items.
  3. Reduce product stock.
  4. Save payment information.

What happens if the order is inserted successfully but updating the product stock fails?

Now the database contains incomplete data.

This is where database transactions become important.

In this article, we will learn how to use transactions with Dapper in ASP.NET Core Web API from the beginning, assuming you have little or no previous experience with transactions.


What is Dapper?

Dapper is a lightweight Object-Relational Mapper (ORM) for .NET.

It allows us to execute SQL queries while reducing the amount of database-related code we need to write.

For example, without Dapper, you may need to write a lot of code to execute SQL commands and convert database results into C# objects.

With Dapper, we can write:

var users = await connection.QueryAsync<User>(
    "SELECT * FROM Users"
);
Enter fullscreen mode Exit fullscreen mode

Dapper executes the SQL query and maps the result to the User class.

Dapper does not hide SQL from you. Instead, it gives you a simple and fast way to work with SQL.


What is a Database Transaction?

A transaction is a group of database operations that should be treated as one unit of work.

The basic idea is:

Operation 1
    +
Operation 2
    +
Operation 3
    =
One Transaction
Enter fullscreen mode Exit fullscreen mode

Either all operations succeed or all operations are rolled back.

For example:

Create Order
     |
     v
Create Order Items
     |
     v
Update Product Stock
     |
     v
Commit
Enter fullscreen mode Exit fullscreen mode

If something fails:

Create Order
     |
     v
Create Order Items
     |
     v
Update Product Stock
     |
     X Error
     |
     v
Rollback
Enter fullscreen mode Exit fullscreen mode

The database returns to the state it had before the transaction started.


Why Do We Need Transactions?

Imagine an e-commerce application.

A customer purchases:

Product: Laptop
Quantity: 1
Price: ₹50,000
Enter fullscreen mode Exit fullscreen mode

Your API performs three operations:

INSERT INTO Orders
Enter fullscreen mode Exit fullscreen mode

Then:

INSERT INTO OrderItems
Enter fullscreen mode Exit fullscreen mode

Then:

UPDATE Products
SET Stock = Stock - 1
Enter fullscreen mode Exit fullscreen mode

Suppose the first two queries succeed but the third query fails.

You would have:

Order created       Yes
Order item created  Yes
Stock updated       No
Enter fullscreen mode Exit fullscreen mode

This creates inconsistent data.

A transaction prevents this situation.

With a transaction:

Start Transaction

    Create Order
          |
    Create Order Item
          |
    Update Stock
          |
       Success
          |
       COMMIT
Enter fullscreen mode Exit fullscreen mode

If any operation fails:

Start Transaction

    Create Order
          |
    Create Order Item
          |
    Update Stock
          |
       Error
          |
      ROLLBACK
Enter fullscreen mode Exit fullscreen mode

ACID Properties

Database transactions are generally described using four important properties called ACID.

1. Atomicity

All operations should succeed or all should fail.

For example:

Insert Order
Insert Order Item
Update Stock
Enter fullscreen mode Exit fullscreen mode

If stock update fails, the order and order item should also be rolled back.


2. Consistency

The database should remain in a valid state before and after the transaction.


3. Isolation

One transaction should not improperly interfere with another transaction.

For example, two customers should not be able to purchase the last available product simultaneously.


4. Durability

Once a transaction is committed, the changes should remain saved even if the application restarts.


How Transactions Work with Dapper

Dapper itself does not provide a completely separate transaction system.

Instead, Dapper works with the transaction functionality provided by the database connection.

For SQL Server, we can use:

var transaction = connection.BeginTransaction();
Enter fullscreen mode Exit fullscreen mode

Then pass the transaction to Dapper:

await connection.ExecuteAsync(
    sql,
    parameters,
    transaction
);
Enter fullscreen mode Exit fullscreen mode

At the end:

transaction.Commit();
Enter fullscreen mode Exit fullscreen mode

If something goes wrong:

transaction.Rollback();
Enter fullscreen mode Exit fullscreen mode

Creating an ASP.NET Core Web API Project

Let's create a simple ASP.NET Core Web API.

You can create the project using:

dotnet new webapi -n DapperTransactionApi
Enter fullscreen mode Exit fullscreen mode

Move into the project:

cd DapperTransactionApi
Enter fullscreen mode Exit fullscreen mode

Install Dapper:

dotnet add package Dapper
Enter fullscreen mode Exit fullscreen mode

For SQL Server, install:

dotnet add package Microsoft.Data.SqlClient
Enter fullscreen mode Exit fullscreen mode

Database Setup

For this example, we will create two tables:

Orders
OrderItems
Enter fullscreen mode Exit fullscreen mode

Our SQL database can contain the following tables.

Orders Table

CREATE TABLE Orders
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    CustomerName NVARCHAR(100) NOT NULL,
    TotalAmount DECIMAL(18,2) NOT NULL,
    CreatedAt DATETIME2 NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

OrderItems Table

CREATE TABLE OrderItems
(
    Id INT IDENTITY(1,1) PRIMARY KEY,
    OrderId INT NOT NULL,
    ProductName NVARCHAR(200) NOT NULL,
    Quantity INT NOT NULL,
    Price DECIMAL(18,2) NOT NULL,

    CONSTRAINT FK_OrderItems_Orders
        FOREIGN KEY (OrderId)
        REFERENCES Orders(Id)
);
Enter fullscreen mode Exit fullscreen mode

Create the Connection String

Open:

appsettings.json
Enter fullscreen mode Exit fullscreen mode

Add your SQL Server connection string:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Enter fullscreen mode Exit fullscreen mode

If you are using SQL Server authentication:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=ShopDb;User Id=sa;Password=YourPassword;TrustServerCertificate=True;"
  }
}
Enter fullscreen mode Exit fullscreen mode

Never hard-code database passwords directly into your source code in a production application.


Create Order Models

Create a folder:

Models
Enter fullscreen mode Exit fullscreen mode

Create:

Order.cs
Enter fullscreen mode Exit fullscreen mode
namespace DapperTransactionApi.Models;

public class Order
{
    public int Id { get; set; }

    public string CustomerName { get; set; } = string.Empty;

    public decimal TotalAmount { get; set; }

    public DateTime CreatedAt { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Now create:

OrderItem.cs
Enter fullscreen mode Exit fullscreen mode
namespace DapperTransactionApi.Models;

public class OrderItem
{
    public int Id { get; set; }

    public int OrderId { get; set; }

    public string ProductName { get; set; } = string.Empty;

    public int Quantity { get; set; }

    public decimal Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Create Request Models

For creating an order, we can create:

CreateOrderRequest.cs
Enter fullscreen mode Exit fullscreen mode
namespace DapperTransactionApi.Models;

public class CreateOrderRequest
{
    public string CustomerName { get; set; } = string.Empty;

    public decimal TotalAmount { get; set; }

    public List<CreateOrderItemRequest> Items { get; set; } = [];
}
Enter fullscreen mode Exit fullscreen mode

Create:

CreateOrderItemRequest.cs
Enter fullscreen mode Exit fullscreen mode
namespace DapperTransactionApi.Models;

public class CreateOrderItemRequest
{
    public string ProductName { get; set; } = string.Empty;

    public int Quantity { get; set; }

    public decimal Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Create Database Connection

Create a folder:

Data
Enter fullscreen mode Exit fullscreen mode

Then create:

DbConnectionFactory.cs
Enter fullscreen mode Exit fullscreen mode
using Microsoft.Data.SqlClient;
using System.Data;

namespace DapperTransactionApi.Data;

public class DbConnectionFactory
{
    private readonly IConfiguration _configuration;

    public DbConnectionFactory(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public IDbConnection CreateConnection()
    {
        return new SqlConnection(
            _configuration.GetConnectionString("DefaultConnection")
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Register the Connection Factory

Open:

Program.cs
Enter fullscreen mode Exit fullscreen mode

Add:

builder.Services.AddScoped<DbConnectionFactory>();
Enter fullscreen mode Exit fullscreen mode

Your basic setup can look like:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddScoped<DbConnectionFactory>();

var app = builder.Build();

app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

Creating an Order with a Transaction

Now comes the important part.

Create:

Controllers/OrdersController.cs
Enter fullscreen mode Exit fullscreen mode
using Dapper;
using DapperTransactionApi.Data;
using DapperTransactionApi.Models;
using Microsoft.AspNetCore.Mvc;

namespace DapperTransactionApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly DbConnectionFactory _connectionFactory;

    public OrdersController(DbConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder(
        CreateOrderRequest request)
    {
        using var connection = _connectionFactory.CreateConnection();

        await connection.OpenAsync();

        using var transaction = connection.BeginTransaction();

        try
        {
            var orderSql = """
                INSERT INTO Orders
                (
                    CustomerName,
                    TotalAmount,
                    CreatedAt
                )
                OUTPUT INSERTED.Id
                VALUES
                (
                    @CustomerName,
                    @TotalAmount,
                    @CreatedAt
                );
                """;

            var orderId = await connection.ExecuteScalarAsync<int>(
                orderSql,
                new
                {
                    request.CustomerName,
                    request.TotalAmount,
                    CreatedAt = DateTime.UtcNow
                },
                transaction
            );

            var itemSql = """
                INSERT INTO OrderItems
                (
                    OrderId,
                    ProductName,
                    Quantity,
                    Price
                )
                VALUES
                (
                    @OrderId,
                    @ProductName,
                    @Quantity,
                    @Price
                );
                """;

            foreach (var item in request.Items)
            {
                await connection.ExecuteAsync(
                    itemSql,
                    new
                    {
                        OrderId = orderId,
                        item.ProductName,
                        item.Quantity,
                        item.Price
                    },
                    transaction
                );
            }

            transaction.Commit();

            return Ok(new
            {
                message = "Order created successfully",
                orderId
            });
        }
        catch (Exception ex)
        {
            transaction.Rollback();

            return StatusCode(500, new
            {
                message = "Order creation failed",
                error = ex.Message
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding the Code Step by Step

Let's understand what is happening.

Step 1: Create Database Connection

using var connection = _connectionFactory.CreateConnection();
Enter fullscreen mode Exit fullscreen mode

This creates a connection to SQL Server.


Step 2: Open the Connection

await connection.OpenAsync();
Enter fullscreen mode Exit fullscreen mode

The connection is now ready for database operations.


Step 3: Start Transaction

using var transaction = connection.BeginTransaction();
Enter fullscreen mode Exit fullscreen mode

This is the most important line.

From this point, database operations can participate in the transaction.


Step 4: Insert the Order

We execute:

INSERT INTO Orders
Enter fullscreen mode Exit fullscreen mode

using Dapper:

var orderId = await connection.ExecuteScalarAsync<int>(
    orderSql,
    parameters,
    transaction
);
Enter fullscreen mode Exit fullscreen mode

Notice this:

transaction
Enter fullscreen mode Exit fullscreen mode

We pass the transaction to Dapper.

This means the query belongs to our transaction.


Why Does ExecuteScalarAsync Return the Order ID?

Our SQL contains:

OUTPUT INSERTED.Id
Enter fullscreen mode Exit fullscreen mode

Suppose the database generates:

Order ID = 101
Enter fullscreen mode Exit fullscreen mode

Then:

ExecuteScalarAsync<int>()
Enter fullscreen mode Exit fullscreen mode

returns:

101
Enter fullscreen mode Exit fullscreen mode

We can then use this ID when inserting order items.


Step 5: Insert Order Items

We loop through all items:

foreach (var item in request.Items)
Enter fullscreen mode Exit fullscreen mode

Then insert each item:

await connection.ExecuteAsync(
    itemSql,
    parameters,
    transaction
);
Enter fullscreen mode Exit fullscreen mode

Again, we pass:

transaction
Enter fullscreen mode Exit fullscreen mode

Therefore, these inserts belong to the same transaction.


Step 6: Commit the Transaction

If everything works:

transaction.Commit();
Enter fullscreen mode Exit fullscreen mode

The database permanently saves all changes.

The flow becomes:

Create Order
     |
     v
Create Order Item 1
     |
     v
Create Order Item 2
     |
     v
Create Order Item 3
     |
     v
Commit
Enter fullscreen mode Exit fullscreen mode

Step 7: Rollback on Error

Suppose the second order item fails.

The code enters:

catch
Enter fullscreen mode Exit fullscreen mode

Then:

transaction.Rollback();
Enter fullscreen mode Exit fullscreen mode

This cancels all changes made by the transaction.

For example:

Order Inserted
     |
Item 1 Inserted
     |
Item 2 Failed
     |
Rollback
Enter fullscreen mode Exit fullscreen mode

After rollback:

Order Inserted = No
Item 1 Inserted = No
Item 2 Inserted = No
Enter fullscreen mode Exit fullscreen mode

This is the main purpose of a transaction.


Important: Every Query Must Use the Transaction

This is a common beginner mistake.

Correct:

await connection.ExecuteAsync(
    sql,
    parameters,
    transaction
);
Enter fullscreen mode Exit fullscreen mode

Incorrect:

await connection.ExecuteAsync(
    sql,
    parameters
);
Enter fullscreen mode Exit fullscreen mode

If you forget to pass the transaction, that query may not participate in your transaction.

The safe pattern is:

connection
+
transaction
+
Dapper query
Enter fullscreen mode Exit fullscreen mode

Example API Request

We can call:

POST /api/orders
Enter fullscreen mode Exit fullscreen mode

with:

{
  "customerName": "Shyam",
  "totalAmount": 1500,
  "items": [
    {
      "productName": "T-Shirt",
      "quantity": 2,
      "price": 500
    },
    {
      "productName": "Jeans",
      "quantity": 1,
      "price": 500
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The API performs:

BEGIN TRANSACTION

Insert Order
       |
       v
Get Order ID
       |
       v
Insert T-Shirt
       |
       v
Insert Jeans
       |
       v
COMMIT
Enter fullscreen mode Exit fullscreen mode

What Happens If an Error Occurs?

Suppose the Jeans insertion fails.

Without a transaction:

Order       -> Created
T-Shirt     -> Created
Jeans       -> Failed
Enter fullscreen mode Exit fullscreen mode

This is bad because the database contains partial data.

With a transaction:

Order       -> Created
T-Shirt     -> Created
Jeans       -> Failed
                 |
                 v
              Rollback
Enter fullscreen mode Exit fullscreen mode

Final database state:

Order       -> Not Created
T-Shirt     -> Not Created
Jeans       -> Not Created
Enter fullscreen mode Exit fullscreen mode

The database remains consistent.


Transaction with Product Stock

A real e-commerce application usually needs more than two queries.

For example:

Create Order
     |
Create Order Items
     |
Reduce Product Stock
     |
Create Payment Record
     |
Commit
Enter fullscreen mode Exit fullscreen mode

All of these operations can be placed inside one transaction.

For example:

using var transaction = connection.BeginTransaction();

try
{
    // Create order

    // Create order items

    // Update product stock

    // Create payment record

    transaction.Commit();
}
catch
{
    transaction.Rollback();

    throw;
}
Enter fullscreen mode Exit fullscreen mode

This is a common pattern in real-world applications.


Transaction Isolation Levels

SQL Server supports different transaction isolation levels.

For example:

using System.Data;

using var transaction =
    connection.BeginTransaction(
        IsolationLevel.ReadCommitted
    );
Enter fullscreen mode Exit fullscreen mode

Some common isolation levels are:

ReadUncommitted
ReadCommitted
RepeatableRead
Serializable
Snapshot
Enter fullscreen mode Exit fullscreen mode

For beginners, ReadCommitted is usually a good starting point to understand.

The correct isolation level depends on your application's concurrency requirements.


Using TransactionScope

Another option in .NET is:

TransactionScope
Enter fullscreen mode Exit fullscreen mode

For example:

using var scope = new TransactionScope(
    TransactionScopeAsyncFlowOption.Enabled
);

try
{
    // Database operations

    scope.Complete();
}
catch
{
    // Transaction automatically rolls back
}
Enter fullscreen mode Exit fullscreen mode

However, when using Dapper with a single database connection, explicitly using:

connection.BeginTransaction()
Enter fullscreen mode Exit fullscreen mode

is often easier to understand and control.


Should We Put Transactions in Controllers?

For small learning projects, putting transaction code in a controller is understandable.

However, in a production application, it is better to separate responsibilities.

A common architecture is:

Controller
     |
     v
Service
     |
     v
Repository
     |
     v
Dapper
     |
     v
SQL Server
Enter fullscreen mode Exit fullscreen mode

For example:

OrdersController
       |
       v
OrderService
       |
       v
OrderRepository
       |
       v
SQL Server
Enter fullscreen mode Exit fullscreen mode

The service layer can control the business transaction.


Recommended Production Structure

A larger ASP.NET Core project could look like:

DapperTransactionApi
│
├── Controllers
│   └── OrdersController.cs
│
├── Services
│   └── OrderService.cs
│
├── Repositories
│   └── OrderRepository.cs
│
├── Models
│   ├── Order.cs
│   ├── OrderItem.cs
│   └── CreateOrderRequest.cs
│
├── Data
│   └── DbConnectionFactory.cs
│
├── appsettings.json
└── Program.cs
Enter fullscreen mode Exit fullscreen mode

This makes the application easier to maintain.


Common Mistakes Beginners Make

1. Forgetting to Commit

If you start a transaction:

var transaction = connection.BeginTransaction();
Enter fullscreen mode Exit fullscreen mode

you need to commit it:

transaction.Commit();
Enter fullscreen mode Exit fullscreen mode

Otherwise, your changes may not become permanent.


2. Forgetting Rollback

Always handle exceptions:

try
{
    // operations

    transaction.Commit();
}
catch
{
    transaction.Rollback();

    throw;
}
Enter fullscreen mode Exit fullscreen mode

3. Not Passing the Transaction to Dapper

Wrong:

await connection.ExecuteAsync(
    sql,
    parameters
);
Enter fullscreen mode Exit fullscreen mode

Correct:

await connection.ExecuteAsync(
    sql,
    parameters,
    transaction
);
Enter fullscreen mode Exit fullscreen mode

4. Opening Multiple Connections

Avoid doing this unnecessarily:

Connection 1 -> Insert Order

Connection 2 -> Insert Order Item
Enter fullscreen mode Exit fullscreen mode

A transaction belongs to a particular database connection.

For a simple transaction, use the same connection:

Connection
    |
    +--- Transaction
           |
           +--- Query 1
           +--- Query 2
           +--- Query 3
Enter fullscreen mode Exit fullscreen mode

5. Keeping Transactions Open for Too Long

Transactions consume database resources.

Avoid:

Begin Transaction

Wait for external API
Wait for user
Perform calculations
Wait for another service

Commit
Enter fullscreen mode Exit fullscreen mode

Instead, keep the transaction as short as reasonably possible.

Begin Transaction
    |
    Database operations
    |
Commit
Enter fullscreen mode Exit fullscreen mode

Transaction vs Normal Database Operations

Without transaction:

Query 1 -> Success
Query 2 -> Success
Query 3 -> Failed
Enter fullscreen mode Exit fullscreen mode

The first two changes may remain in the database.

With transaction:

Query 1 -> Success
Query 2 -> Success
Query 3 -> Failed
                 |
                 v
              Rollback
Enter fullscreen mode Exit fullscreen mode

Everything is reverted.


When Should You Use a Transaction?

Use transactions when multiple database operations must succeed together.

Common examples include:

E-commerce

Create Order
Create Order Items
Update Stock
Create Payment
Enter fullscreen mode Exit fullscreen mode

Banking

Debit Account A
Credit Account B
Create Transaction Record
Enter fullscreen mode Exit fullscreen mode

Wallet

Deduct User Wallet
Add Merchant Wallet
Create Payment Record
Enter fullscreen mode Exit fullscreen mode

Booking System

Create Booking
Reserve Seat
Create Payment Record
Enter fullscreen mode Exit fullscreen mode

Inventory

Create Purchase
Add Stock
Create Stock History
Enter fullscreen mode Exit fullscreen mode

When Do You Not Need a Transaction?

You usually don't need an explicit transaction for a single simple database operation.

For example:

SELECT * FROM Products
Enter fullscreen mode Exit fullscreen mode

or:

UPDATE Products
SET Name = @Name
WHERE Id = @Id
Enter fullscreen mode Exit fullscreen mode

If there is only one independent operation, an explicit transaction may add unnecessary complexity.


Dapper Transaction Best Practices

For production applications, follow these practices:

1. Use parameterized queries

Good:

var sql = """
    SELECT *
    FROM Products
    WHERE Id = @Id
    """;

await connection.QueryAsync<Product>(
    sql,
    new { Id = productId }
);
Enter fullscreen mode Exit fullscreen mode

Avoid building SQL using string concatenation:

var sql =
    "SELECT * FROM Products WHERE Id = " + productId;
Enter fullscreen mode Exit fullscreen mode

Parameterized queries help protect against SQL injection.


2. Keep transactions short

Do only the required database operations inside the transaction.


3. Use async APIs

For ASP.NET Core applications:

await connection.ExecuteAsync(...);
Enter fullscreen mode Exit fullscreen mode

is preferable to blocking database calls.


4. Handle exceptions properly

Use:

try
{
    // transaction operations

    transaction.Commit();
}
catch
{
    transaction.Rollback();

    throw;
}
Enter fullscreen mode Exit fullscreen mode

5. Keep business logic outside the controller

For larger applications, use:

Controller
Service
Repository
Database
Enter fullscreen mode Exit fullscreen mode

instead of putting everything into the controller.


Complete Transaction Flow

The complete flow can be visualized as:

                HTTP Request
                     |
                     v
              OrdersController
                     |
                     v
                OrderService
                     |
                     v
               Open Connection
                     |
                     v
              Begin Transaction
                     |
          +----------+----------+
          |          |          |
          v          v          v
      Insert       Insert     Update
       Order      Items       Stock
          |          |          |
          +----------+----------+
                     |
                     v
               Everything OK?
                  /       \
                Yes        No
                 |          |
                 v          v
              COMMIT     ROLLBACK
                 |          |
                 +-----+----+
                       |
                       v
                   API Response
Enter fullscreen mode Exit fullscreen mode

This is the basic concept you should remember.


Final Example to Remember

If you are new to transactions, remember this simple pattern:

using var connection = _connectionFactory.CreateConnection();

await connection.OpenAsync();

using var transaction = connection.BeginTransaction();

try
{
    await connection.ExecuteAsync(
        sql1,
        parameters1,
        transaction
    );

    await connection.ExecuteAsync(
        sql2,
        parameters2,
        transaction
    );

    await connection.ExecuteAsync(
        sql3,
        parameters3,
        transaction
    );

    transaction.Commit();
}
catch
{
    transaction.Rollback();

    throw;
}
Enter fullscreen mode Exit fullscreen mode

Think of it like this:

BEGIN
  |
  +-- Query 1
  |
  +-- Query 2
  |
  +-- Query 3
  |
  +-- Success --> COMMIT
  |
  +-- Error   --> ROLLBACK
Enter fullscreen mode Exit fullscreen mode

Once you understand this pattern, you can use Dapper transactions for orders, payments, wallets, bookings, inventory, and many other business operations.


Conclusion

Transactions are an important part of building reliable ASP.NET Core Web APIs.

When multiple database operations belong to the same business operation, a transaction ensures that the database does not end up with partially completed data.

With Dapper, the basic transaction pattern is straightforward:

Open Connection
       ↓
Begin Transaction
       ↓
Execute Dapper Queries
       ↓
If Successful → Commit
       ↓
If Failed → Rollback
Enter fullscreen mode Exit fullscreen mode

The most important thing to remember is that every Dapper query that should participate in the transaction must use the same database connection and transaction object.

Once this concept is clear, you can move toward a cleaner architecture using:

Controller
    ↓
Service
    ↓
Repository
    ↓
Dapper
    ↓
SQL Server
Enter fullscreen mode Exit fullscreen mode

This approach is particularly useful when building production-level ASP.NET Core APIs such as e-commerce, payment, inventory, booking, and wallet systems.

Top comments (0)