Optimization & Best Practices
Level: Intermediate
Dapper is fast by default, but understanding caching, repository patterns, and common pitfalls ensures optimal performance and clean code structure.
- Dapper caching: Query plans cached automatically
- Repository pattern: Abstract data access
- Connection pooling: Handled automatically
- Buffering: Default buffered: true, use false for streaming
- Chunking: Process large datasets in batches
- School Management patterns: StudentRepository, ExamRepository patterns
- Common mistakes: Creating new connections per query, not using parameters, no error handling
Repository Pattern - Clean Data Access
public interface IStudentRepository
{
Task<List<Student>> GetByClassAsync(string className);
Task<Student> GetByIdAsync(int id);
Task<int> CreateAsync(Student student);
Task UpdateAsync(Student student);
Task DeleteAsync(int id);
}
public class StudentRepository : IStudentRepository
{
private readonly string _connString;
public StudentRepository(IConfiguration config)
{
_connString = config.GetConnectionString("DefaultConnection");
}
private IDbConnection GetConnection() => new SqlConnection(_connString);
public async Task<List<Student>> GetByClassAsync(string className)
{
using (var conn = GetConnection())
{
return (await conn.QueryAsync<Student>(
"SELECT Id, Name, ClassName FROM Students WHERE ClassName = @className",
new { className = className }
)).ToList();
}
}
public async Task<Student> GetByIdAsync(int id)
{
using (var conn = GetConnection())
{
return await conn.QueryFirstOrDefaultAsync<Student>(
"SELECT Id, Name, ClassName FROM Students WHERE Id = @id",
new { id = id }
);
}
}
public async Task<int> CreateAsync(Student student)
{
using (var conn = GetConnection())
{
return await conn.QuerySingleAsync<int>(
@"INSERT INTO Students (Name, ClassName, Status)
VALUES (@name, @className, @status);
SELECT SCOPE_IDENTITY();",
student
);
}
}
public async Task UpdateAsync(Student student)
{
using (var conn = GetConnection())
{
await conn.ExecuteAsync(
"UPDATE Students SET Name = @name, ClassName = @className WHERE Id = @id",
student
);
}
}
public async Task DeleteAsync(int id)
{
using (var conn = GetConnection())
{
await conn.ExecuteAsync(
"DELETE FROM Students WHERE Id = @id",
new { id = id }
);
}
}
}
Register Repository in DI
// Program.cs
builder.Services.AddScoped<IStudentRepository, StudentRepository>();
Use in Controller
[ApiController]
[Route("api/students")]
public class StudentsController : ControllerBase
{
private readonly IStudentRepository _repository;
public StudentsController(IStudentRepository repository)
{
_repository = repository;
}
[HttpGet]
public async Task<ActionResult<List<Student>>> GetByClass(string className)
{
var students = await _repository.GetByClassAsync(className);
return Ok(students);
}
[HttpGet("{id}")]
public async Task<ActionResult<Student>> GetStudent(int id)
{
var student = await _repository.GetByIdAsync(id);
if (student == null)
return NotFound();
return Ok(student);
}
}
Batch Processing for Large Datasets
public async Task BatchImportStudentsAsync(List<Student> students)
{
const int batchSize = 1000;
for (int i = 0; i < students.Count; i += batchSize)
{
var batch = students.Skip(i).Take(batchSize).ToList();
using (var conn = GetConnection())
{
await conn.OpenAsync();
using (var trans = conn.BeginTransaction())
{
try
{
foreach (var student in batch)
{
await conn.ExecuteAsync(
@"INSERT INTO Students (Name, ClassName, Status)
VALUES (@name, @className, @status)",
student,
transaction: trans
);
}
trans.Commit();
}
catch
{
trans.Rollback();
throw;
}
}
}
}
}
Connection Pool Tuning
Dapper uses automatic connection pooling. Configure in connection string:
"Server=localhost;Database=SmsDb;Min Pool Size=5;Max Pool Size=100;Connection Lifetime=3600;"
Error Handling Pattern
public async Task<bool> UpdateStudentAsync(Student student)
{
try
{
using (var conn = GetConnection())
{
var result = await conn.ExecuteAsync(
"UPDATE Students SET Name = @name WHERE Id = @id",
student
);
return result > 0;
}
}
catch (SqlException ex)
{
// Log error
_logger.LogError(ex, "Failed to update student {StudentId}", student.Id);
throw;
}
}
Best Practices Summary
✓ Use repository pattern for abstraction ✓ Always use async methods ✓ Use parameters (never concatenate) ✓ Handle errors gracefully ✓ Batch large operations ✓ Use connection pooling (automatic) ✓ Dispose connections properly (using statement)
- Fast: Minimal overhead, direct SQL execution
- Simple: No complex ORM configuration
- Flexible: Write exact SQL you need
- Testable: Easy to mock repositories
Use ChatGPT, Claude, or Copilot to go deeper on Dapper Optimization & Best Practices. Try these prompts:
"What's the repository pattern and why use it?""How do you handle large dataset imports efficiently?""What error handling patterns work well with Dapper?""How does Dapper's performance compare to EF Core?"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.
Complete Learning Path
You've learned:
- ADO.NET: Manual, low-level control
- EF Core: High-level ORM with automatic SQL generation
- Dapper: Lightweight SQL-first approach
Choose based on needs:
- ADO.NET: Complex queries, stored procedures, raw performance
- EF Core: Rapid development, complex relationships, migrations
- Dapper: Balance of control and simplicity, performance-critical code