Dapper in .NET Core: The Ultimate Guide 🚀

Dayanand Thombare
13 min readMay 4, 2024

--

Welcome to the ultimate guide on Dapper in .NET Core! 🌟 In this in-depth article, we will dive into the world of Dapper and explore its features, benefits, and how to use it effectively in your .NET Core applications. As a professional .NET and C# Architect with over 30 years of experience, I will guide you through the process of setting up Dapper, understanding its advantages, and providing real-world examples that follow SOLID principles, clean code, maintainability, scalability, and readability. 📚

Photo by Carl Heyerdahl on Unsplash

What is Dapper?

Dapper is a lightweight, high-performance data access tool built by the Stack Overflow team. It provides a minimalist approach to data access layer in .NET applications, focusing on raw performance while maintaining the simplicity of your code. Dapper extends the IDbConnection interface with additional methods to perform SQL commands and query data.

Dapper is a lightweight, high-performance micro-ORM (Object-Relational Mapper) for .NET. It simplifies database access by providing a convenient way to map database results to strongly-typed objects. Dapper is designed to be fast, efficient, and easy to use, making it an excellent choice for developers who want to maximize performance and productivity. 🎯

Why Use Dapper? 🤔

Dapper offers several advantages over traditional ORMs and manual database access:

  1. Performance: Dapper is incredibly fast and efficient. It minimizes the overhead of mapping results to objects, resulting in high-performance database access. 🏎️
  2. Simplicity: Dapper provides a simple and intuitive API for executing queries and mapping results. It allows you to write SQL queries directly, giving you full control over your database interactions. 🧩
  3. Flexibility: Dapper supports a wide range of database providers, including SQL Server, MySQL, PostgreSQL, and more. It also integrates seamlessly with existing ADO.NET code. 🌐
  4. Lightweight: Dapper is a lightweight library with minimal dependencies. It doesn’t impose any complex configuration or setup, making it easy to integrate into your projects. 🪶

Setting Up Dapper in a .NET Core Project

To get started with Dapper, you will need a .NET Core environment set up. Here’s a quick rundown:

Create a new .NET Core application:

dotnet new console -n DapperDemo
cd DapperDemo

Add the Dapper NuGet package:

dotnet add package Dapper

Add the necessary using statements in your code:

using Dapper;
using System.Data;
using System.Data.SqlClient;

Configure your database connection string in the appsettings.json file:

{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyDatabase;User Id=myuser;Password=mypassword;"
}
}

Now you’re ready to start using Dapper in your .NET Core application! 🎉

Basic Examples with Dapper

Let’s start with some simple CRUD operations to understand the basic usage of Dapper.

Basic Dapper Usage

Let’s start with a simple example to demonstrate the basic usage of Dapper. Suppose we have a Customer table in our database, and we want to retrieve all customers.

Problem Statement:

Retrieve all customers from the Customer table and map the results to a Customer object.

Solution:

public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}

public async Task<IEnumerable<Customer>> GetAllCustomers()
{
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();
var customers = await connection.QueryAsync<Customer>("SELECT * FROM Customer");
return customers;
}
}

In this example, we define a Customer class that represents the structure of the Customer table. We then use Dapper's QueryAsync method to execute a simple SELECT query and map the results to a collection of Customer objects.

Notice how we use the using statement to ensure proper disposal of the database connection and the async/await pattern for asynchronous database access. This follows best practices for resource management and non-blocking I/O. 🔧

Stored Procedures

Dapper seamlessly integrates with stored procedures, allowing you to execute them and map the results to objects.

Problem Statement

Execute a stored procedure that retrieves customers by their email domain.

Solution

public async Task<IEnumerable<Customer>> GetCustomersByEmailDomain(string domain)
{
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();
var customers = await connection.QueryAsync<Customer>(
"GetCustomersByEmailDomain",
new { EmailDomain = domain },
commandType: CommandType.StoredProcedure);

return customers;
}
}

In this example, we assume there is a stored procedure named GetCustomersByEmailDomain that takes an email domain as a parameter and returns the corresponding customers.

We use Dapper’s QueryAsync method to execute the stored procedure, passing the EmailDomain parameter as an anonymous object. We specify the commandType parameter as CommandType.StoredProcedure to indicate that we're executing a stored procedure.

Dapper automatically maps the result set to a collection of Customer objects, making it easy to work with the retrieved data. 😊

Advanced Dapper Concepts

Now let’s explore some advanced concepts and scenarios where Dapper truly shines. 🌟

Query Multiple Results:

Dapper allows you to execute queries that return multiple result sets using the QueryMultiple method. This is useful when you need to retrieve data from multiple tables or stored procedures in a single database call.

Problem Statement

Retrieve customers and their corresponding orders in a single database query.

Solution

public async Task<(IEnumerable<Customer>, IEnumerable<Order>)> GetCustomersWithOrders()
{
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();

using (var multi = await connection.QueryMultipleAsync(@"
SELECT * FROM Customer;
SELECT * FROM Order WHERE CustomerId IN (SELECT Id FROM Customer);"))
{
var customers = await multi.ReadAsync<Customer>();
var orders = await multi.ReadAsync<Order>();

return (customers, orders);
}
}
}

In this example, we use the QueryMultipleAsync method to execute two separate queries in a single database call. The first query retrieves all customers, and the second query retrieves the orders associated with those customers.

We use the ReadAsync method to read each result set and map it to the corresponding Customer and Order objects. Finally, we return a tuple containing the customers and their orders.

This approach optimizes database round-trips and improves performance by fetching related data in a single call. 🚀

Example: Transaction Management

Dapper supports database transactions, allowing you to group multiple database operations into a single atomic unit of work.

Problem Statement

Transfer funds from one account to another within a transaction.

Solution

public async Task TransferFunds(int fromAccountId, int toAccountId, decimal amount)
{
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();

using (var transaction = await connection.BeginTransactionAsync())
{
try
{
// Deduct funds from the source account
await connection.ExecuteAsync(
"UPDATE Account SET Balance = Balance - @Amount WHERE Id = @FromAccountId",
new { FromAccountId = fromAccountId, Amount = amount },
transaction: transaction);

// Add funds to the destination account
await connection.ExecuteAsync(
"UPDATE Account SET Balance = Balance + @Amount WHERE Id = @ToAccountId",
new { ToAccountId = toAccountId, Amount = amount },
transaction: transaction);

await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
}

In this example, we use a transaction to ensure the atomicity of the fund transfer operation. We start a transaction using the BeginTransactionAsync method and pass the transaction object to the ExecuteAsync method for each database operation.

If any exception occurs during the transaction, we roll back the transaction using the RollbackAsync method to revert any changes made. If all operations succeed, we commit the transaction using the CommitAsync method.

By using transactions, we maintain data integrity and ensure that the fund transfer is an all-or-nothing operation. 💰

Asynchronous Operations

Dapper fully supports asynchronous database operations, allowing you to write non-blocking and scalable code.

Problem Statement

Retrieve customers asynchronously and process them concurrently.

Solution

public async Task ProcessCustomersAsync()
{
using (var connection = new SqlConnection(_connectionString))
{
await connection.OpenAsync();

var customers = await connection.QueryAsync<Customer>("SELECT * FROM Customer");

var tasks = customers.Select(async customer =>
{
// Process each customer asynchronously
await ProcessCustomer(customer);
});

await Task.WhenAll(tasks);
}
}

private async Task ProcessCustomer(Customer customer)
{
// Perform some asynchronous processing for each customer
await Task.Delay(1000); // Simulating async work
Console.WriteLine($"Processed customer: {customer. Name}");
}

In this example, we use the QueryAsync method to retrieve all customers asynchronously. We then use LINQ's Select method to create a collection of tasks, where each task represents the asynchronous processing of a customer.

We pass each customer to the ProcessCustomer method, which performs some asynchronous work (simulated with Task.Delay in this example).

Finally, we use Task.WhenAll to wait for all the customer processing tasks to complete before exiting the method.

This approach leverages the power of asynchronous programming to process customers concurrently, improving the overall performance and responsiveness of the application. ⏱️

Handling Complex Relationships

Problem Statement: Query an Order and its associated Items using multi-mapping.

public async Task<Order> GetOrderWithItemsAsync(int orderId)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var query = @"
SELECT o.*, i.*
FROM Orders o
INNER JOIN Items i ON o.Id = i.OrderId
WHERE o.Id = @OrderId";
var orderDict = new Dictionary<int, Order>();

var order = await connection.QueryAsync<Order, Item, Order>(
query,
(order, item) =>
{
if (!orderDict.TryGetValue(order.Id, out var currentOrder))
{
currentOrder = order;
currentOrder.Items = new List<Item>();
orderDict.Add(currentOrder.Id, currentOrder);
}
currentOrder.Items.Add(item);
return currentOrder;
},
new { OrderId = orderId },
splitOn: "Id"
).Distinct().SingleOrDefault();

return order;
}

Going Further: Dynamic Queries and Performance

Example : Dynamic Filtering

Problem Statement: Generate a dynamic query based on optional filter parameters for user searches.

public async Task<IEnumerable<User>> SearchUsersAsync(string name, string email)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var sql = new StringBuilder("SELECT * FROM Users WHERE 1 = 1");
var parameters = new DynamicParameters();

if (!string.IsNullOrEmpty(name))
{
sql.Append(" AND Name LIKE @Name");
parameters.Add("Name", $"%{name}%");
}
if (!string.IsNullOrEmpty(email))
{
sql.Append(" AND Email LIKE @Email");
parameters.Add("Email", $"%{email}%");
}

return await connection.QueryAsync<User>(sql.ToString(), parameters);
}

Example : Performance Optimization with Buffered Queries

Problem Statement: Optimize data retrieval operations to handle large datasets efficiently.

public async Task<IEnumerable<User>> GetAllUsersBufferedAsync()
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var query = "SELECT * FROM Users";
// Setting buffered to false is useful for processing large data streams without holding everything in memory
var users = await connection.QueryAsync<User>(query, buffered: false);
return users;
}

Example : Custom Mapping

Problem Statement: Customize the mapping of database columns to properties in your entity that do not follow conventional naming.

public class CustomUser
{
public int UserId { get; set; } // Custom mapping from 'ID' column
public string UserName { get; set; } // Custom mapping from 'Name' column
}

public async Task<IEnumerable<CustomUser>> GetAllCustomUsersAsync()
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
var query = "SELECT ID as UserId, Name as UserName FROM Users";
return await connection.QueryAsync<CustomUser>(query);
}

Example : Bulk Operations

Problem Statement: Efficiently handle bulk insert operations to minimize database round-trips.

public async Task InsertUsersBulkAsync(List<User> users)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var dataTable = new DataTable();
dataTable.Columns.Add("Name", typeof(string));
dataTable.Columns.Add("Email", typeof(string));

foreach (var user in users)
{
dataTable.Rows.Add(user.Name, user.Email);
}

using var bulkCopy = new SqlBulkCopy(connection)
{
DestinationTableName = "Users",
BatchSize = 1000
};
bulkCopy.ColumnMappings.Add("Name", "Name");
bulkCopy.ColumnMappings.Add("Email", "Email");

await bulkCopy.WriteToServerAsync(dataTable);
}

Handling Errors and Exceptions

Example : Robust Error Handling

Problem Statement: Implement a robust error handling mechanism to manage database-related exceptions gracefully.

public async Task UpdateUserAsync(User user)
{
using var connection = new SqlConnection(connectionString);
try
{
await connection.OpenAsync();
var query = "UPDATE Users SET Name = @Name, Email = @Email WHERE Id = @Id";
var result = await connection.ExecuteAsync(query, user);
if (result == 0)
{
throw new InvalidOperationException("No user found with the specified ID.");
}
}
catch (SqlException ex)
{
// Log error or handle it based on the exception type
Console.WriteLine($"Database operation failed: {ex.Message}");
throw; // Rethrow to handle it further up the call stack
}
}

Scalability and Maintainability Considerations

  1. Connection Management: Use dependency injection to manage database connections, which enhances scalability by handling multiple requests more efficiently.
  2. Query Separation: Keep your SQL queries separate from C# code, possibly in stored procedures or a dedicated class, to improve maintainability.
  3. Performance Profiling: Regularly profile your application to identify and optimize slow database queries.

Integrating Dapper with Modern .NET Core Features

Example : Using Dapper with Dependency Injection

Problem Statement: Implement Dapper in a .NET Core application using dependency injection for better manageability and testing.

public interface IDatabaseService
{
Task<IEnumerable<User>> GetAllUsersAsync();
}

public class DatabaseService : IDatabaseService
{
private readonly IConfiguration _configuration;

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

public async Task<IEnumerable<User>> GetAllUsersAsync()
{
var connectionString = _configuration.GetConnectionString("DefaultConnection");
using var connection = new SqlConnection(connectionString);
return await connection.QueryAsync<User>("SELECT * FROM Users");
}
}

// In your Startup.cs or Program.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IDatabaseService, DatabaseService>();
}

This integration with dependency injection not only makes your application cleaner but also enhances its testability by allowing you to mock database operations in unit tests.

Example : Combining Dapper with CQRS for Scalable Architectures

Problem Statement: Design a scalable architecture using the Command Query Responsibility Segregation (CQRS) pattern with Dapper.

// Command Model
public class CreateUserCommand
{
public string Name { get; set; }
public string Email { get; set; }
}

public class CommandHandler
{
private readonly IDbConnection _connection;

public CommandHandler(IDbConnection connection)
{
_connection = connection;
}

public async Task<int> Handle(CreateUserCommand command)
{
var sql = "INSERT INTO Users (Name, Email) VALUES (@Name, @Email)";
return await _connection.ExecuteAsync(sql, command);
}
}

// Query Model
public class UserQuery
{
private readonly IDbConnection _connection;

public UserQuery(IDbConnection connection)
{
_connection = connection;
}

public async Task<IEnumerable<User>> GetAllUsers()
{
return await _connection.QueryAsync<User>("SELECT * FROM Users");
}
}

This example shows how to separate the reading and writing operations, which is essential for systems that scale and evolve over time. This separation allows for more optimized performance and scalability.

Example : Advanced Performance Tuning with Dapper

Problem Statement: Optimize the performance of Dapper in scenarios dealing with large datasets.

public async Task<IEnumerable<User>> GetUsersWithOptimizedPerformance()
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var query = "SELECT * FROM Users";
// Use AsQueryable to defer execution and reduce memory footprint
return connection.Query<User>(query).AsQueryable().Take(1000);
}

Example : Integrating Dapper with Microservices

Problem Statement: Utilize Dapper within a microservices architecture to ensure lightweight and efficient database interactions.

public class UserService : IUserService
{
private readonly IDbConnection _connection;

public UserService(IDbConnection connection)
{
_connection = connection;
}

public async Task<User> GetUserByIdAsync(int id)
{
var query = "SELECT * FROM Users WHERE Id = @Id";
return await _connection.QuerySingleOrDefaultAsync<User>(query, new { Id = id });
}
}

// Setup in Startup.cs for a Microservice
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IDbConnection>(sp =>
new SqlConnection(Configuration.GetConnectionString("UserServiceDB")));
services.AddScoped<IUserService, UserService>();
}

This setup shows how Dapper can be effectively used in a microservices architecture, providing each service with its own lightweight data access mechanism that does not burden the service with heavy ORM libraries.

Example : Using Dapper with Event Sourcing

Problem Statement: Implement event sourcing with Dapper to capture all changes to an application state as a sequence of events.

public class EventStore : IEventStore
{
private readonly IDbConnection _connection;

public EventStore(IDbConnection connection)
{
_connection = connection;
}

public async Task SaveEventsAsync(Guid aggregateId, IEnumerable<Event> events, int expectedVersion)
{
foreach (var event in events)
{
var query = "INSERT INTO Events (AggregateId, EventData, Version) VALUES (@AggregateId, @EventData, @Version)";
await _connection.ExecuteAsync(query, new {
AggregateId = aggregateId,
EventData = JsonConvert.SerializeObject(event),
Version = ++expectedVersion
});
}
}
}

This approach allows the system to be more resilient and provides a complete audit trail of changes, which is particularly beneficial in systems requiring high levels of traceability and auditability.

Example : Optimizing Dapper with Memory Caching

Problem Statement: Reduce database load by implementing memory caching strategies with Dapper.

public class CachedUserService : IUserService
{
private readonly IDbConnection _connection;
private readonly IMemoryCache _cache;

public CachedUserService(IDbConnection connection, IMemoryCache cache)
{
_connection = connection;
_cache = cache;
}

public async Task<User> GetUserByIdAsync(int id)
{
if (!_cache.TryGetValue($"User_{id}", out User user))
{
user = await _connection.QuerySingleOrDefaultAsync<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = id });
_cache.Set($"User_{id}", user, TimeSpan.FromMinutes(30));
}
return user;
}
}

This example demonstrates how adding a caching layer can dramatically improve the responsiveness of your application by reducing the number of queries to the database, thereby lowering the overall load.

Example : Asynchronous Streaming with Dapper

Problem Statement: Efficiently handle large data streams with Dapper to prevent high memory consumption and improve application responsiveness.

public async IAsyncEnumerable<User> StreamUsersAsync()
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var query = "SELECT * FROM Users";
var reader = await connection.ExecuteReaderAsync(query);
while (await reader.ReadAsync())
{
yield return new User
{
Id = reader.GetInt32(reader.GetOrdinal("Id")),
Name = reader.GetString(reader.GetOrdinal("Name")),
Email = reader.GetString(reader.GetOrdinal("Email"))
};
}
}

This implementation leverages C# 8.0’s asynchronous streams (IAsyncEnumerable) to efficiently process rows as they are read from the database, which is particularly useful for handling large datasets without consuming extensive system resources.

Example : Advanced Security Practices

Problem Statement: Implement enhanced security measures to protect sensitive data accessed through Dapper.

public async Task<User> GetUserSecurelyAsync(int id)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
var query = "SELECT Id, Name, Email FROM Users WHERE Id = @Id FOR JSON PATH, WITHOUT_ARRAY_WRAPPER";
var result = await connection.QuerySingleOrDefaultAsync<string>(query, new { Id = id });
var user = JsonConvert.DeserializeObject<User>(result);
// Assume some decryption logic here if necessary
return user;
}

// Example using SQL Server's FOR JSON to secure data format and minimize injection risks

Incorporating advanced security features, such as JSON output modes for safer data handling and potential encryption/decryption practices, ensures that your application remains secure against emerging threats.

Best Practices and Tips

Here are some best practices and tips to keep in mind when using Dapper in your .NET Core applications:

  1. Use parameterized queries: Always use parameterized queries to prevent SQL injection attacks and improve performance. Dapper supports named and positional parameters. 🛡️
  2. Dispose connections and transactions: Ensure that you properly dispose of database connections and transactions using the using statement or by explicitly calling the Dispose method. This helps in efficient resource management. ♻️
  3. Use asynchronous methods: Leverage Dapper’s asynchronous methods (QueryAsync, ExecuteAsync, etc.) to avoid blocking threads and improve scalability. 🧵
  4. Map to strongly-typed objects: Map query results to strongly-typed objects to benefit from compile-time type checking and better code maintainability. 💪
  5. Handle null values: Be cautious when mapping results to objects with non-nullable properties. Use nullable types or provide default values to handle null database values gracefully. 🎭
  6. Profile and optimize queries: Use profiling tools to identify slow queries and optimize them. Dapper’s performance is excellent, but it’s still important to write efficient SQL queries. 🔍
  7. Use transactions judiciously: Use transactions only when necessary to maintain data integrity. Avoid long-running transactions that can impact concurrency and performance. 🔒
  8. Consider caching: Implement caching mechanisms to store frequently accessed data and reduce database round-trips. Dapper plays well with caching libraries like Redis or in-memory caching. 📥

Conclusion

Dapper is a powerful and efficient micro-ORM that simplifies database access in .NET Core applications. Its simplicity, performance, and flexibility make it an excellent choice for developers who value speed and control over their database interactions.

Throughout this article, we explored various aspects of Dapper, from basic usage to advanced concepts like multiple result sets, stored procedures, transactions, and asynchronous operations. We also discussed best practices and tips to help you write clean, maintainable, and scalable code.

Remember, the key to success with Dapper lies in understanding its strengths, applying best practices, and leveraging its features effectively. By following the principles of SOLID, clean code, and performance optimization, you can build high-quality and efficient .NET Core applications that leverage the power of Dapper.

So go ahead, dive into the world of Dapper, and unleash the potential of high-performance database access in your .NET Core projects! 🚀

Happy coding! 💻✨

--

--