
Table of Contents
Dapper Fundamentals and Setup
Basic Query Operations and Mapping
Advanced Query Techniques
Performance Optimization Strategies
Multi-Mapping and Complex Relationships
Stored Procedures and Dynamic Parameters
Bulk Operations and Performance
Real-World Enterprise Scenarios
Integration with ASP.NET Core
Best Practices and Advanced Patterns
1. Dapper Fundamentals and Setup
What is Dapper and Why Use It?
Dapper is a simple object mapper for .NET that extends IDbConnection with useful methods for executing queries and mapping results to objects. It's known for its exceptional performance and minimal overhead.
Key Advantages:
Nearly as fast as raw ADO.NET
Easy to use and learn
Minimal configuration required
Flexible mapping capabilities
Supports complex object graphs
Project Setup and Configuration
Program.cs
using Dapper;
using Microsoft.Data.SqlClient;
using System.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to container
builder.Services.AddControllers();
builder.Services.AddScoped<IDbConnection>(provider =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
return new SqlConnection(connectionString);
});
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
var app = builder.Build();
// Configure pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.MapControllers();
app.Run();
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=DapperDemo;Trusted_Connection=true;TrustServerCertificate=true;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Database Setup and Initialization
DatabaseInitializer.cs
using Dapper;
using Microsoft.Data.SqlClient;
using System.Data;
namespace DapperDeepDive.Data
{
public class DatabaseInitializer
{
private readonly string _connectionString;
public DatabaseInitializer(string connectionString)
{
_connectionString = connectionString;
}
public async Task InitializeDatabaseAsync()
{
using var connection = new SqlConnection(_connectionString);
// Create database if not exists
var createDbSql = @"
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'DapperDemo')
BEGIN
CREATE DATABASE DapperDemo;
END";
await connection.ExecuteAsync(createDbSql);
// Switch to DapperDemo database
connection.ConnectionString += "Database=DapperDemo;";
await connection.OpenAsync();
// Create tables
await CreateTablesAsync(connection);
await SeedSampleDataAsync(connection);
}
private async Task CreateTablesAsync(IDbConnection connection)
{
// Users table
var createUsersTable = @"
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Users' AND xtype='U')
CREATE TABLE Users (
Id INT IDENTITY PRIMARY KEY,
Username NVARCHAR(50) NOT NULL UNIQUE,
Email NVARCHAR(100) NOT NULL UNIQUE,
FirstName NVARCHAR(50) NOT NULL,
LastName NVARCHAR(50) NOT NULL,
DateOfBirth DATE NULL,
CreatedAt DATETIME2 DEFAULT GETUTCDATE(),
IsActive BIT DEFAULT 1
)";
// Products table
var createProductsTable = @"
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Products' AND xtype='U')
CREATE TABLE Products (
Id INT IDENTITY PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Description NVARCHAR(500) NULL,
Price DECIMAL(18,2) NOT NULL,
StockQuantity INT NOT NULL DEFAULT 0,
Category NVARCHAR(50) NULL,
CreatedAt DATETIME2 DEFAULT GETUTCDATE(),
IsActive BIT DEFAULT 1
)";
// Orders table
var createOrdersTable = @"
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Orders' AND xtype='U')
CREATE TABLE Orders (
Id INT IDENTITY PRIMARY KEY,
UserId INT NOT NULL FOREIGN KEY REFERENCES Users(Id),
OrderDate DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
TotalAmount DECIMAL(18,2) NOT NULL,
Status NVARCHAR(20) NOT NULL DEFAULT 'Pending',
ShippingAddress NVARCHAR(200) NULL,
CreatedAt DATETIME2 DEFAULT GETUTCDATE()
)";
// OrderItems table
var createOrderItemsTable = @"
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='OrderItems' AND xtype='U')
CREATE TABLE OrderItems (
Id INT IDENTITY PRIMARY KEY,
OrderId INT NOT NULL FOREIGN KEY REFERENCES Orders(Id),
ProductId INT NOT NULL FOREIGN KEY REFERENCES Products(Id),
Quantity INT NOT NULL,
UnitPrice DECIMAL(18,2) NOT NULL,
TotalPrice DECIMAL(18,2) NOT NULL
)";
await connection.ExecuteAsync(createUsersTable);
await connection.ExecuteAsync(createProductsTable);
await connection.ExecuteAsync(createOrdersTable);
await connection.ExecuteAsync(createOrderItemsTable);
}
private async Task SeedSampleDataAsync(IDbConnection connection)
{
// Check if data already exists
var userCount = await connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM Users");
if (userCount > 0) return;
// Insert sample users
var insertUsers = @"
INSERT INTO Users (Username, Email, FirstName, LastName, DateOfBirth) VALUES
('john_doe', '[email protected]', 'John', 'Doe', '1985-05-15'),
('jane_smith', '[email protected]', 'Jane', 'Smith', '1990-08-22'),
('bob_wilson', '[email protected]', 'Bob', 'Wilson', '1988-12-10'),
('alice_brown', '[email protected]', 'Alice', 'Brown', '1992-03-30')";
// Insert sample products
var insertProducts = @"
INSERT INTO Products (Name, Description, Price, StockQuantity, Category) VALUES
('Laptop', 'High-performance laptop', 999.99, 50, 'Electronics'),
('Smartphone', 'Latest smartphone model', 699.99, 100, 'Electronics'),
('Headphones', 'Noise-cancelling headphones', 199.99, 75, 'Electronics'),
('Book', 'Programming guide', 39.99, 200, 'Books'),
('Desk Chair', 'Ergonomic office chair', 299.99, 30, 'Furniture')";
await connection.ExecuteAsync(insertUsers);
await connection.ExecuteAsync(insertProducts);
}
}
}
2. Basic Query Operations and Mapping
Entity Models
Models/User.cs
namespace DapperDeepDive.Models
{
public class User
{
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime? DateOfBirth { get; set; }
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
// Navigation properties
public List<Order> Orders { get; set; } = new List<Order>();
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public string Category { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public bool IsActive { get; set; }
}
public class Order
{
public int Id { get; set; }
public int UserId { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; } = string.Empty;
public string ShippingAddress { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
// Navigation properties
public User User { get; set; } = new User();
public List<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
}
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
// Navigation properties
public Product Product { get; set; } = new Product();
}
}
Basic Repository Implementation
Repositories/IUserRepository.cs
using DapperDeepDive.Models;
namespace DapperDeepDive.Repositories
{
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
Task<IEnumerable<User>> GetAllAsync();
Task<User?> GetByUsernameAsync(string username);
Task<User?> GetByEmailAsync(string email);
Task<int> CreateAsync(User user);
Task<bool> UpdateAsync(User user);
Task<bool> DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
Task<int> GetCountAsync();
}
}
Repositories/UserRepository.cs
using Dapper;
using DapperDeepDive.Models;
using System.Data;
namespace DapperDeepDive.Repositories
{
public class UserRepository : IUserRepository
{
private readonly IDbConnection _connection;
public UserRepository(IDbConnection connection)
{
_connection = connection;
}
public async Task<User?> GetByIdAsync(int id)
{
const string sql = @"
SELECT Id, Username, Email, FirstName, LastName,
DateOfBirth, CreatedAt, IsActive
FROM Users
WHERE Id = @Id AND IsActive = 1";
return await _connection.QueryFirstOrDefaultAsync<User>(sql, new { Id = id });
}
public async Task<IEnumerable<User>> GetAllAsync()
{
const string sql = @"
SELECT Id, Username, Email, FirstName, LastName,
DateOfBirth, CreatedAt, IsActive
FROM Users
WHERE IsActive = 1
ORDER BY CreatedAt DESC";
return await _connection.QueryAsync<User>(sql);
}
public async Task<User?> GetByUsernameAsync(string username)
{
const string sql = @"
SELECT Id, Username, Email, FirstName, LastName,
DateOfBirth, CreatedAt, IsActive
FROM Users
WHERE Username = @Username AND IsActive = 1";
return await _connection.QueryFirstOrDefaultAsync<User>(sql, new { Username = username });
}
public async Task<User?> GetByEmailAsync(string email)
{
const string sql = @"
SELECT Id, Username, Email, FirstName, LastName,
DateOfBirth, CreatedAt, IsActive
FROM Users
WHERE Email = @Email AND IsActive = 1";
return await _connection.QueryFirstOrDefaultAsync<User>(sql, new { Email = email });
}
public async Task<int> CreateAsync(User user)
{
const string sql = @"
INSERT INTO Users (Username, Email, FirstName, LastName, DateOfBirth)
OUTPUT INSERTED.Id
VALUES (@Username, @Email, @FirstName, @LastName, @DateOfBirth)";
return await _connection.ExecuteScalarAsync<int>(sql, user);
}
public async Task<bool> UpdateAsync(User user)
{
const string sql = @"
UPDATE Users
SET Username = @Username, Email = @Email,
FirstName = @FirstName, LastName = @LastName,
DateOfBirth = @DateOfBirth
WHERE Id = @Id AND IsActive = 1";
var affectedRows = await _connection.ExecuteAsync(sql, user);
return affectedRows > 0;
}
public async Task<bool> DeleteAsync(int id)
{
const string sql = @"
UPDATE Users
SET IsActive = 0
WHERE Id = @Id";
var affectedRows = await _connection.ExecuteAsync(sql, new { Id = id });
return affectedRows > 0;
}
public async Task<bool> ExistsAsync(int id)
{
const string sql = "SELECT 1 FROM Users WHERE Id = @Id AND IsActive = 1";
var result = await _connection.ExecuteScalarAsync<int?>(sql, new { Id = id });
return result.HasValue;
}
public async Task<int> GetCountAsync()
{
const string sql = "SELECT COUNT(*) FROM Users WHERE IsActive = 1";
return await _connection.ExecuteScalarAsync<int>(sql);
}
}
}
Product Repository with Advanced Features
Repositories/ProductRepository.cs
using Dapper;
using DapperDeepDive.Models;
using System.Data;
namespace DapperDeepDive.Repositories
{
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task<IEnumerable<Product>> GetByCategoryAsync(string category);
Task<IEnumerable<Product>> GetActiveProductsAsync();
Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm);
Task<decimal> GetTotalInventoryValueAsync();
Task<Dictionary<string, int>> GetProductCountByCategoryAsync();
Task<int> CreateAsync(Product product);
Task<bool> UpdateAsync(Product product);
Task<bool> UpdateStockAsync(int productId, int newStockQuantity);
Task<bool> DeleteAsync(int id);
}
public class ProductRepository : IProductRepository
{
private readonly IDbConnection _connection;
public ProductRepository(IDbConnection connection)
{
_connection = connection;
}
public async Task<Product?> GetByIdAsync(int id)
{
const string sql = @"
SELECT Id, Name, Description, Price, StockQuantity,
Category, CreatedAt, IsActive
FROM Products
WHERE Id = @Id";
return await _connection.QueryFirstOrDefaultAsync<Product>(sql, new { Id = id });
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
const string sql = @"
SELECT Id, Name, Description, Price, StockQuantity,
Category, CreatedAt, IsActive
FROM Products
ORDER BY CreatedAt DESC";
return await _connection.QueryAsync<Product>(sql);
}
public async Task<IEnumerable<Product>> GetByCategoryAsync(string category)
{
const string sql = @"
SELECT Id, Name, Description, Price, StockQuantity,
Category, CreatedAt, IsActive
FROM Products
WHERE Category = @Category AND IsActive = 1
ORDER BY Name";
return await _connection.QueryAsync<Product>(sql, new { Category = category });
}
public async Task<IEnumerable<Product>> GetActiveProductsAsync()
{
const string sql = @"
SELECT Id, Name, Description, Price, StockQuantity,
Category, CreatedAt, IsActive
FROM Products
WHERE IsActive = 1 AND StockQuantity > 0
ORDER BY Price DESC";
return await _connection.QueryAsync<Product>(sql);
}
public async Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm)
{
const string sql = @"
SELECT Id, Name, Description, Price, StockQuantity,
Category, CreatedAt, IsActive
FROM Products
WHERE (Name LIKE '%' + @SearchTerm + '%'
OR Description LIKE '%' + @SearchTerm + '%')
AND IsActive = 1
ORDER BY
CASE
WHEN Name LIKE @SearchTerm + '%' THEN 1
WHEN Name LIKE '%' + @SearchTerm + '%' THEN 2
ELSE 3
END,
Name";
return await _connection.QueryAsync<Product>(sql, new { SearchTerm = searchTerm });
}
public async Task<decimal> GetTotalInventoryValueAsync()
{
const string sql = @"
SELECT SUM(Price * StockQuantity)
FROM Products
WHERE IsActive = 1";
return await _connection.ExecuteScalarAsync<decimal>(sql);
}
public async Task<Dictionary<string, int>> GetProductCountByCategoryAsync()
{
const string sql = @"
SELECT Category, COUNT(*) as Count
FROM Products
WHERE IsActive = 1
GROUP BY Category
ORDER BY Count DESC";
var results = await _connection.QueryAsync<(string Category, int Count)>(sql);
return results.ToDictionary(x => x.Category, x => x.Count);
}
public async Task<int> CreateAsync(Product product)
{
const string sql = @"
INSERT INTO Products (Name, Description, Price, StockQuantity, Category)
OUTPUT INSERTED.Id
VALUES (@Name, @Description, @Price, @StockQuantity, @Category)";
return await _connection.ExecuteScalarAsync<int>(sql, product);
}
public async Task<bool> UpdateAsync(Product product)
{
const string sql = @"
UPDATE Products
SET Name = @Name, Description = @Description,
Price = @Price, StockQuantity = @StockQuantity,
Category = @Category, IsActive = @IsActive
WHERE Id = @Id";
var affectedRows = await _connection.ExecuteAsync(sql, product);
return affectedRows > 0;
}
public async Task<bool> UpdateStockAsync(int productId, int newStockQuantity)
{
const string sql = @"
UPDATE Products
SET StockQuantity = @StockQuantity
WHERE Id = @ProductId";
var affectedRows = await _connection.ExecuteAsync(sql,
new { ProductId = productId, StockQuantity = newStockQuantity });
return affectedRows > 0;
}
public async Task<bool> DeleteAsync(int id)
{
const string sql = "DELETE FROM Products WHERE Id = @Id";
var affectedRows = await _connection.ExecuteAsync(sql, new { Id = id });
return affectedRows > 0;
}
}
}
3. Advanced Query Techniques
Multiple Result Sets
Repositories/AdvancedUserRepository.cs
using Dapper;
using DapperDeepDive.Models;
using System.Data;
namespace DapperDeepDive.Repositories
{
public interface IAdvancedUserRepository
{
Task<(User User, IEnumerable<Order> Orders)> GetUserWithOrdersAsync(int userId);
Task<UserStatistics> GetUserStatisticsAsync(int userId);
Task<PaginatedResult<User>> GetUsersPaginatedAsync(int pageNumber, int pageSize);
Task<IEnumerable<User>> GetUsersWithOrdersAsync();
Task<dynamic> GetUserDynamicAsync(int userId);
}
public class AdvancedUserRepository : IAdvancedUserRepository
{
private readonly IDbConnection _connection;
public AdvancedUserRepository(IDbConnection connection)
{
_connection = connection;
}
public async Task<(User User, IEnumerable<Order> Orders)> GetUserWithOrdersAsync(int userId)
{
const string sql = @"
SELECT * FROM Users WHERE Id = @UserId;
SELECT * FROM Orders WHERE UserId = @UserId ORDER BY OrderDate DESC;";
using var multi = await _connection.QueryMultipleAsync(sql, new { UserId = userId });
var user = await multi.ReadFirstOrDefaultAsync<User>();
var orders = await multi.ReadAsync<Order>();
return (user, orders);
}
public async Task<UserStatistics> GetUserStatisticsAsync(int userId)
{
const string sql = @"
SELECT
COUNT(o.Id) as TotalOrders,
SUM(o.TotalAmount) as TotalSpent,
AVG(o.TotalAmount) as AverageOrderValue,
MAX(o.OrderDate) as LastOrderDate,
MIN(o.OrderDate) as FirstOrderDate
FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
WHERE u.Id = @UserId
GROUP BY u.Id, u.Username";
return await _connection.QueryFirstOrDefaultAsync<UserStatistics>(sql, new { UserId = userId });
}
public async Task<PaginatedResult<User>> GetUsersPaginatedAsync(int pageNumber, int pageSize)
{
var offset = (pageNumber - 1) * pageSize;
const string sql = @"
SELECT
Id, Username, Email, FirstName, LastName,
DateOfBirth, CreatedAt, IsActive
FROM Users
WHERE IsActive = 1
ORDER BY CreatedAt DESC
OFFSET @Offset ROWS
FETCH NEXT @PageSize ROWS ONLY;
SELECT COUNT(*)
FROM Users
WHERE IsActive = 1;";
using var multi = await _connection.QueryMultipleAsync(sql,
new { Offset = offset, PageSize = pageSize });
var users = await multi.ReadAsync<User>();
var totalCount = await multi.ReadFirstAsync<int>();
return new PaginatedResult<User>(users, totalCount, pageNumber, pageSize);
}
public async Task<IEnumerable<User>> GetUsersWithOrdersAsync()
{
const string sql = @"
SELECT
u.*,
o.Id, o.UserId, o.OrderDate, o.TotalAmount, o.Status, o.ShippingAddress
FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
WHERE u.IsActive = 1
ORDER BY u.CreatedAt DESC, o.OrderDate DESC";
var userDict = new Dictionary<int, User>();
var users = await _connection.QueryAsync<User, Order, User>(
sql,
(user, order) =>
{
if (!userDict.TryGetValue(user.Id, out var userEntry))
{
userEntry = user;
userEntry.Orders = new List<Order>();
userDict.Add(userEntry.Id, userEntry);
}
if (order != null)
{
userEntry.Orders.Add(order);
}
return userEntry;
},
splitOn: "Id");
return userDict.Values;
}
public async Task<dynamic> GetUserDynamicAsync(int userId)
{
const string sql = @"
SELECT
u.Id, u.Username, u.Email,
COUNT(o.Id) as OrderCount,
SUM(o.TotalAmount) as TotalSpent
FROM Users u
LEFT JOIN Orders o ON u.Id = o.UserId
WHERE u.Id = @UserId
GROUP BY u.Id, u.Username, u.Email";
return await _connection.QueryFirstOrDefaultAsync<dynamic>(sql, new { UserId = userId });
}
}
public class UserStatistics
{
public int TotalOrders { get; set; }
public decimal TotalSpent { get; set; }
public decimal AverageOrderValue { get; set; }
public DateTime? LastOrderDate { get; set; }
public DateTime? FirstOrderDate { get; set; }
}
public class PaginatedResult<T>
{
public IEnumerable<T> Items { get; set; }
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPrevious => PageNumber > 1;
public bool HasNext => PageNumber < TotalPages;
public PaginatedResult(IEnumerable<T> items, int totalCount, int pageNumber, int pageSize)
{
Items = items;
TotalCount = totalCount;
PageNumber = pageNumber;
PageSize = pageSize;
}
}
}
Advanced Query Building
Services/QueryBuilderService.cs
using Dapper;
using DapperDeepDive.Models;
using System.Data;
using System.Text;
namespace DapperDeepDive.Services
{
public interface IQueryBuilderService
{
Task<IEnumerable<Product>> SearchProductsAsync(ProductSearchCriteria criteria);
Task<IEnumerable<User>> GetUsersByCriteriaAsync(UserSearchCriteria criteria);
string BuildProductSearchQuery(ProductSearchCriteria criteria, out DynamicParameters parameters);
string BuildUserSearchQuery(UserSearchCriteria criteria, out DynamicParameters parameters);
}
public class QueryBuilderService : IQueryBuilderService
{
private readonly IDbConnection _connection;
public QueryBuilderService(IDbConnection connection)
{
_connection = connection;
}
public async Task<IEnumerable<Product>> SearchProductsAsync(ProductSearchCriteria criteria)
{
var query = BuildProductSearchQuery(criteria, out var parameters);
return await _connection.QueryAsync<Product>(query, parameters);
}
public async Task<IEnumerable<User>> GetUsersByCriteriaAsync(UserSearchCriteria criteria)
{
var query = BuildUserSearchQuery(criteria, out var parameters);
return await _connection.QueryAsync<User>(query, parameters);
}
public string BuildProductSearchQuery(ProductSearchCriteria criteria, out DynamicParameters parameters)
{
parameters = new DynamicParameters();
var sqlBuilder = new StringBuilder("SELECT * FROM Products WHERE 1=1");
if (!string.IsNullOrEmpty(criteria.Name))
{
sqlBuilder.Append(" AND Name LIKE '%' + @Name + '%'");
parameters.Add("Name", criteria.Name);
}
if (!string.IsNullOrEmpty(criteria.Category))
{
sqlBuilder.Append(" AND Category = @Category");
parameters.Add("Category", criteria.Category);
}
if (criteria.MinPrice.HasValue)
{
sqlBuilder.Append(" AND Price >= @MinPrice");
parameters.Add("MinPrice", criteria.MinPrice.Value);
}
if (criteria.MaxPrice.HasValue)
{
sqlBuilder.Append(" AND Price <= @MaxPrice");
parameters.Add("MaxPrice", criteria.MaxPrice.Value);
}
if (criteria.InStockOnly)
{
sqlBuilder.Append(" AND StockQuantity > 0");
}
if (criteria.IsActive.HasValue)
{
sqlBuilder.Append(" AND IsActive = @IsActive");
parameters.Add("IsActive", criteria.IsActive.Value);
}
// Order by
sqlBuilder.Append(" ORDER BY ");
sqlBuilder.Append(criteria.SortBy?.ToLower() switch
{
"name" => "Name",
"price" => "Price",
"created" => "CreatedAt",
_ => "CreatedAt"
});
sqlBuilder.Append(criteria.SortDescending ? " DESC" : " ASC");
// Pagination
if (criteria.PageSize > 0 && criteria.PageNumber > 0)
{
var offset = (criteria.PageNumber - 1) * criteria.PageSize;
sqlBuilder.Append(" OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY");
parameters.Add("Offset", offset);
parameters.Add("PageSize", criteria.PageSize);
}
return sqlBuilder.ToString();
}
public string BuildUserSearchQuery(UserSearchCriteria criteria, out DynamicParameters parameters)
{
parameters = new DynamicParameters();
var sqlBuilder = new StringBuilder("SELECT * FROM Users WHERE 1=1");
if (!string.IsNullOrEmpty(criteria.Username))
{
sqlBuilder.Append(" AND Username LIKE '%' + @Username + '%'");
parameters.Add("Username", criteria.Username);
}
if (!string.IsNullOrEmpty(criteria.Email))
{
sqlBuilder.Append(" AND Email LIKE '%' + @Email + '%'");
parameters.Add("Email", criteria.Email);
}
if (!string.IsNullOrEmpty(criteria.FirstName))
{
sqlBuilder.Append(" AND FirstName LIKE '%' + @FirstName + '%'");
parameters.Add("FirstName", criteria.FirstName);
}
if (!string.IsNullOrEmpty(criteria.LastName))
{
sqlBuilder.Append(" AND LastName LIKE '%' + @LastName + '%'");
parameters.Add("LastName", criteria.LastName);
}
if (criteria.CreatedAfter.HasValue)
{
sqlBuilder.Append(" AND CreatedAt >= @CreatedAfter");
parameters.Add("CreatedAfter", criteria.CreatedAfter.Value);
}
if (criteria.CreatedBefore.HasValue)
{
sqlBuilder.Append(" AND CreatedAt <= @CreatedBefore");
parameters.Add("CreatedBefore", criteria.CreatedBefore.Value);
}
if (criteria.IsActive.HasValue)
{
sqlBuilder.Append(" AND IsActive = @IsActive");
parameters.Add("IsActive", criteria.IsActive.Value);
}
// Order by
sqlBuilder.Append(" ORDER BY ");
sqlBuilder.Append(criteria.SortBy?.ToLower() switch
{
"username" => "Username",
"email" => "Email",
"created" => "CreatedAt",
_ => "CreatedAt"
});
sqlBuilder.Append(criteria.SortDescending ? " DESC" : " ASC");
// Pagination
if (criteria.PageSize > 0 && criteria.PageNumber > 0)
{
var offset = (criteria.PageNumber - 1) * criteria.PageSize;
sqlBuilder.Append(" OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY");
parameters.Add("Offset", offset);
parameters.Add("PageSize", criteria.PageSize);
}
return sqlBuilder.ToString();
}
}
public class ProductSearchCriteria
{
public string? Name { get; set; }
public string? Category { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public bool InStockOnly { get; set; } = false;
public bool? IsActive { get; set; } = true;
public string? SortBy { get; set; } = "created";
public bool SortDescending { get; set; } = true;
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
public class UserSearchCriteria
{
public string? Username { get; set; }
public string? Email { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateTime? CreatedAfter { get; set; }
public DateTime? CreatedBefore { get; set; }
public bool? IsActive { get; set; } = true;
public string? SortBy { get; set; } = "created";
public bool SortDescending { get; set; } = true;
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
}
4. Performance Optimization Strategies
Connection Management and Pooling
Services/ConnectionFactory.cs
using Microsoft.Data.SqlClient;
using System.Data;
namespace DapperDeepDive.Services
{
public interface IConnectionFactory
{
IDbConnection CreateConnection();
Task<IDbConnection> CreateOpenConnectionAsync();
}
public class ConnectionFactory : IConnectionFactory
{
private readonly string _connectionString;
public ConnectionFactory(string connectionString)
{
_connectionString = connectionString;
}
public IDbConnection CreateConnection()
{
var connection = new SqlConnection(_connectionString);
// Configure connection for optimal performance
var builder = new SqlConnectionStringBuilder(_connectionString)
{
MaxPoolSize = 100,
MinPoolSize = 0,
Pooling = true,
ConnectionTimeout = 30,
CommandTimeout = 30
};
connection.ConnectionString = builder.ConnectionString;
return connection;
}
public async Task<IDbConnection> CreateOpenConnectionAsync()
{
var connection = CreateConnection();
await connection.OpenAsync();
return connection;
}
}
}
Performance Monitoring
Services/PerformanceMonitor.cs
using Dapper;
using System.Data;
using System.Diagnostics;
namespace DapperDeepDive.Services
{
public interface IPerformanceMonitor
{
Task<T> MonitorQueryAsync<T>(Func<Task<T>> query, string operationName);
Task MonitorActionAsync(Func<Task> action, string operationName);
void LogPerformanceMetrics(string operation, TimeSpan duration, int? recordsAffected = null);
}
public class PerformanceMonitor : IPerformanceMonitor
{
private readonly ILogger<PerformanceMonitor> _logger;
public PerformanceMonitor(ILogger<PerformanceMonitor> logger)
{
_logger = logger;
}
public async Task<T> MonitorQueryAsync<T>(Func<Task<T>> query, string operationName)
{
var stopwatch = Stopwatch.StartNew();
try
{
var result = await query();
stopwatch.Stop();
LogPerformanceMetrics(operationName, stopwatch.Elapsed);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Error executing {OperationName} after {ElapsedMs}ms",
operationName, stopwatch.ElapsedMilliseconds);
throw;
}
}
public async Task MonitorActionAsync(Func<Task> action, string operationName)
{
var stopwatch = Stopwatch.StartNew();
try
{
await action();
stopwatch.Stop();
LogPerformanceMetrics(operationName, stopwatch.Elapsed);
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(ex, "Error executing {OperationName} after {ElapsedMs}ms",
operationName, stopwatch.ElapsedMilliseconds);
throw;
}
}
public void LogPerformanceMetrics(string operation, TimeSpan duration, int? recordsAffected = null)
{
var logMessage = $"Operation '{operation}' completed in {duration.TotalMilliseconds}ms";
if (recordsAffected.HasValue)
{
logMessage += $", Records affected: {recordsAffected.Value}";
}
if (duration.TotalMilliseconds > 1000)
{
_logger.LogWarning(logMessage);
}
else if (duration.TotalMilliseconds > 100)
{
_logger.LogInformation(logMessage);
}
else
{
_logger.LogDebug(logMessage);
}
}
}
// Performance-optimized repository
public class OptimizedProductRepository
{
private readonly IDbConnection _connection;
private readonly IPerformanceMonitor _performanceMonitor;
public OptimizedProductRepository(IDbConnection connection, IPerformanceMonitor performanceMonitor)
{
_connection = connection;
_performanceMonitor = performanceMonitor;
}
public async Task<IEnumerable<Product>> GetProductsOptimizedAsync()
{
return await _performanceMonitor.MonitorQueryAsync(
async () =>
{
const string sql = @"
SELECT
Id, Name, Price, StockQuantity, Category
FROM Products
WHERE IsActive = 1
ORDER BY CreatedAt DESC";
return await _connection.QueryAsync<Product>(sql);
},
"GetProductsOptimized");
}
public async Task<Product?> GetProductByIdOptimizedAsync(int id)
{
return await _performanceMonitor.MonitorQueryAsync(
async () =>
{
const string sql = @"
SELECT
Id, Name, Price, StockQuantity, Category
FROM Products
WHERE Id = @Id";
return await _connection.QueryFirstOrDefaultAsync<Product>(sql, new { Id = id });
},
"GetProductByIdOptimized");
}
public async Task<bool> BulkUpdateProductsAsync(IEnumerable<Product> products)
{
return await _performanceMonitor.MonitorQueryAsync(
async () =>
{
const string sql = @"
UPDATE Products
SET Price = @Price, StockQuantity = @StockQuantity
WHERE Id = @Id";
var affectedRows = await _connection.ExecuteAsync(sql, products);
return affectedRows > 0;
},
"BulkUpdateProducts");
}
}
}
Caching Strategies
Services/CacheService.cs
using Microsoft.Extensions.Caching.Memory;
namespace DapperDeepDive.Services
{
public interface ICacheService
{
Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null);
void Remove(string key);
void Clear();
}
public class CacheService : ICacheService
{
private readonly IMemoryCache _memoryCache;
private readonly ILogger<CacheService> _logger;
public CacheService(IMemoryCache memoryCache, ILogger<CacheService> logger)
{
_memoryCache = memoryCache;
_logger = logger;
}
public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null)
{
if (_memoryCache.TryGetValue(key, out T cachedValue))
{
_logger.LogDebug("Cache hit for key: {Key}", key);
return cachedValue;
}
_logger.LogDebug("Cache miss for key: {Key}", key);
var value = await factory();
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(5)
};
_memoryCache.Set(key, value, cacheOptions);
return value;
}
public void Remove(string key)
{
_memoryCache.Remove(key);
_logger.LogDebug("Cache removed for key: {Key}", key);
}
public void Clear()
{
// Note: IMemoryCache doesn't have a Clear method by default
// This would need to be implemented with a custom cache implementation
// For now, we'll log a warning
_logger.LogWarning("Clear operation not supported by default IMemoryCache");
}
}
// Cached repository example
public class CachedProductRepository
{
private readonly IProductRepository _productRepository;
private readonly ICacheService _cacheService;
public CachedProductRepository(IProductRepository productRepository, ICacheService cacheService)
{
_productRepository = productRepository;
_cacheService = cacheService;
}
public async Task<Product?> GetByIdAsync(int id)
{
var cacheKey = $"product_{id}";
return await _cacheService.GetOrCreateAsync(cacheKey,
() => _productRepository.GetByIdAsync(id),
TimeSpan.FromMinutes(10));
}
public async Task<IEnumerable<Product>> GetByCategoryAsync(string category)
{
var cacheKey = $"products_category_{category}";
return await _cacheService.GetOrCreateAsync(cacheKey,
() => _productRepository.GetByCategoryAsync(category),
TimeSpan.FromMinutes(5));
}
public async Task<bool> UpdateAsync(Product product)
{
var result = await _productRepository.UpdateAsync(product);
if (result)
{
// Invalidate cache for this product
_cacheService.Remove($"product_{product.Id}");
// Invalidate category cache as well
_cacheService.Remove($"products_category_{product.Category}");
}
return result;
}
}
}
5. Multi-Mapping and Complex Relationships
One-to-Many Relationships
Repositories/OrderRepository.cs
using Dapper;
using DapperDeepDive.Models;
using System.Data;
namespace DapperDeepDive.Repositories
{
public interface IOrderRepository
{
Task<Order?> GetOrderWithDetailsAsync(int orderId);
Task<IEnumerable<Order>> GetUserOrdersWithDetailsAsync(int userId);
Task<OrderSummary> GetOrderSummaryAsync(int orderId);
Task<int> CreateOrderWithItemsAsync(Order order, List<OrderItem> items);
Task<bool> UpdateOrderStatusAsync(int orderId, string status);
}
public class OrderRepository : IOrderRepository
{
private readonly IDbConnection _connection;
public OrderRepository(IDbConnection connection)
{
_connection = connection;
}
public async Task<Order?> GetOrderWithDetailsAsync(int orderId)
{
const string sql = @"
SELECT
o.*,
u.*,
oi.*,
p.*
FROM Orders o
INNER JOIN Users u ON o.UserId = u.Id
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
LEFT JOIN Products p ON oi.ProductId = p.Id
WHERE o.Id = @OrderId
ORDER BY oi.Id";
var orderDict = new Dictionary<int, Order>();
var order = await _connection.QueryAsync<Order, User, OrderItem, Product, Order>(
sql,
(order, user, orderItem, product) =>
{
if (!orderDict.TryGetValue(order.Id, out var orderEntry))
{
orderEntry = order;
orderEntry.User = user;
orderEntry.OrderItems = new List<OrderItem>();
orderDict.Add(orderEntry.Id, orderEntry);
}
if (orderItem != null)
{
orderItem.Product = product;
orderEntry.OrderItems.Add(orderItem);
}
return orderEntry;
},
new { OrderId = orderId },
splitOn: "Id,Id,Id");
return orderDict.Values.FirstOrDefault();
}
public async Task<IEnumerable<Order>> GetUserOrdersWithDetailsAsync(int userId)
{
const string sql = @"
SELECT
o.*,
oi.*,
p.*
FROM Orders o
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
LEFT JOIN Products p ON oi.ProductId = p.Id
WHERE o.UserId = @UserId
ORDER BY o.OrderDate DESC, oi.Id";
var orderDict = new Dictionary<int, Order>();
var orders = await _connection.QueryAsync<Order, OrderItem, Product, Order>(
sql,
(order, orderItem, product) =>
{
if (!orderDict.TryGetValue(order.Id, out var orderEntry))
{
orderEntry = order;
orderEntry.OrderItems = new List<OrderItem>();
orderDict.Add(orderEntry.Id, orderEntry);
}
if (orderItem != null)
{
orderItem.Product = product;
orderEntry.OrderItems.Add(orderItem);
}
return orderEntry;
},
new { UserId = userId },
splitOn: "Id,Id");
return orderDict.Values;
}
public async Task<OrderSummary> GetOrderSummaryAsync(int orderId)
{
const string sql = @"
SELECT
o.Id,
o.OrderDate,
o.TotalAmount,
o.Status,
u.Username,
u.Email,
COUNT(oi.Id) as ItemCount,
SUM(oi.Quantity) as TotalQuantity
FROM Orders o
INNER JOIN Users u ON o.UserId = u.Id
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
WHERE o.Id = @OrderId
GROUP BY o.Id, o.OrderDate, o.TotalAmount, o.Status, u.Username, u.Email";
return await _connection.QueryFirstOrDefaultAsync<OrderSummary>(sql, new { OrderId = orderId });
}
public async Task<int> CreateOrderWithItemsAsync(Order order, List<OrderItem> items)
{
using var transaction = _connection.BeginTransaction();
try
{
// Insert order
const string orderSql = @"
INSERT INTO Orders (UserId, OrderDate, TotalAmount, Status, ShippingAddress)
OUTPUT INSERTED.Id
VALUES (@UserId, @OrderDate, @TotalAmount, @Status, @ShippingAddress)";
var orderId = await _connection.ExecuteScalarAsync<int>(orderSql, order, transaction);
// Insert order items
const string itemsSql = @"
INSERT INTO OrderItems (OrderId, ProductId, Quantity, UnitPrice, TotalPrice)
VALUES (@OrderId, @ProductId, @Quantity, @UnitPrice, @TotalPrice)";
foreach (var item in items)
{
item.OrderId = orderId;
await _connection.ExecuteAsync(itemsSql, item, transaction);
}
transaction.Commit();
return orderId;
}
catch
{
transaction.Rollback();
throw;
}
}
public async Task<bool> UpdateOrderStatusAsync(int orderId, string status)
{
const string sql = "UPDATE Orders SET Status = @Status WHERE Id = @OrderId";
var affectedRows = await _connection.ExecuteAsync(sql,
new { OrderId = orderId, Status = status });
return affectedRows > 0;
}
}
public class OrderSummary
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public int ItemCount { get; set; }
public int TotalQuantity { get; set; }
}
}
Many-to-Many Relationships
Repositories/TagRepository.cs
using Dapper;
using DapperDeepDive.Models;
using System.Data;
namespace DapperDeepDive.Repositories
{
public class Tag
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
public class ProductTag
{
public int ProductId { get; set; }
public int TagId { get; set; }
public DateTime AssociatedAt { get; set; }
}
public interface ITagRepository
{
Task<IEnumerable<Tag>> GetProductTagsAsync(int productId);
Task<IEnumerable<Product>> GetProductsByTagAsync(int tagId);
Task<bool> AddTagToProductAsync(int productId, int tagId);
Task<bool> RemoveTagFromProductAsync(int productId, int tagId);
Task<IEnumerable<Product>> GetProductsByMultipleTagsAsync(IEnumerable<int> tagIds);
}
public class TagRepository : ITagRepository
{
private readonly IDbConnection _connection;
public TagRepository(IDbConnection connection)
{
_connection = connection;
}
public async Task<IEnumerable<Tag>> GetProductTagsAsync(int productId)
{
const string sql = @"
SELECT t.*
FROM Tags t
INNER JOIN ProductTags pt ON t.Id = pt.TagId
WHERE pt.ProductId = @ProductId
ORDER BY t.Name";
return await _connection.QueryAsync<Tag>(sql, new { ProductId = productId });
}
public async Task<IEnumerable<Product>> GetProductsByTagAsync(int tagId)
{
const string sql = @"
SELECT p.*
FROM Products p
INNER JOIN ProductTags pt ON p.Id = pt.ProductId
WHERE pt.TagId = @TagId AND p.IsActive = 1
ORDER BY p.Name";
return await _connection.QueryAsync<Product>(sql, new { TagId = tagId });
}
public async Task<bool> AddTagToProductAsync(int productId, int tagId)
{
const string sql = @"
INSERT INTO ProductTags (ProductId, TagId, AssociatedAt)
VALUES (@ProductId, @TagId, GETUTCDATE())";
try
{
var affectedRows = await _connection.ExecuteAsync(sql,
new { ProductId = productId, TagId = tagId });
return affectedRows > 0;
}
catch (SqlException ex) when (ex.Number == 2627) // Primary key violation
{
// Tag already associated with product
return false;
}
}
public async Task<bool> RemoveTagFromProductAsync(int productId, int tagId)
{
const string sql = @"
DELETE FROM ProductTags
WHERE ProductId = @ProductId AND TagId = @TagId";
var affectedRows = await _connection.ExecuteAsync(sql,
new { ProductId = productId, TagId = tagId });
return affectedRows > 0;
}
public async Task<IEnumerable<Product>> GetProductsByMultipleTagsAsync(IEnumerable<int> tagIds)
{
const string sql = @"
SELECT p.*
FROM Products p
WHERE p.Id IN (
SELECT pt.ProductId
FROM ProductTags pt
WHERE pt.TagId IN @TagIds
GROUP BY pt.ProductId
HAVING COUNT(DISTINCT pt.TagId) = @TagCount
) AND p.IsActive = 1
ORDER BY p.Name";
var tagIdList = tagIds.ToList();
return await _connection.QueryAsync<Product>(sql,
new { TagIds = tagIdList, TagCount = tagIdList.Count });
}
}
}
6. Stored Procedures and Dynamic Parameters
Stored Procedure Integration
Repositories/StoredProcedureRepository.cs

Join the conversation! Your thoughts help the community grow.