Введение в Dapper: что это и зачем нужно
Dapper — это легковесная библиотека (Micro ORM) для платформы .NET, разработанная командой Stack Overflow. Она представляет собой расширение для IDbConnection и предназначена для выполнения SQL-запросов к базам данных с минимальными накладными расходами. В отличие от тяжеловесных ORM (например, Entity Framework Core), Dapper не отслеживает изменения объектов, не генерирует сложные SQL-запросы и не требует настройки контекста данных. Его главная цель — максимальная производительность и простота.
Основные преимущества Dapper:
- Скорость: Dapper работает практически со скоростью сырых ADO.NET команд, так как использует динамическую генерацию IL-кода для маппинга результатов.
- Простота: Минимум кода для выполнения запросов. Не нужно создавать сложные конфигурации.
- Контроль над SQL: Вы пишете SQL-запросы вручную, что даёт полный контроль над тем, какие данные и как извлекаются.
- Гибкость: Работает с любыми ADO.NET провайдерами (SQL Server, PostgreSQL, MySQL, SQLite и др.).
- Расширяемость: Легко интегрируется с существующими проектами и другими ORM.
Dapper идеально подходит для проектов, где важна производительность и прямой контроль над SQL, а также для микросервисов и высоконагруженных приложений.
Установка Dapper
Dapper распространяется через NuGet — менеджер пакетов для .NET. Установить его можно несколькими способами:
Через консоль диспетчера пакетов (Visual Studio):
Install-Package Dapper
Через .NET CLI (рекомендуемый способ):
dotnet add package Dapper
Через интерфейс NuGet в Visual Studio:
- Правый клик на проекте → "Управление пакетами NuGet".
- Перейти на вкладку "Обзор".
- Найти "Dapper" и нажать "Установить".
После установки в проекте появится ссылка на сборку Dapper.dll. Никаких дополнительных зависимостей не требуется — библиотека работает поверх стандартного System.Data.
Быстрый старт с примером кода
Рассмотрим минимальный пример работы с Dapper. Предположим, у нас есть таблица Users в базе данных SQL Server:
CREATE TABLE Users (
Id INT PRIMARY KEY IDENTITY,
Name NVARCHAR(100) NOT NULL,
Age INT NOT NULL
);
Код на C# для получения всех пользователей:
using System.Data.SqlClient;
using Dapper;
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
string connectionString = "Server=localhost;Database=TestDB;Trusted_Connection=True;";
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
// Простой SELECT запрос
var users = connection.Query<User>("SELECT * FROM Users");
foreach (var user in users)
{
Console.WriteLine($"{user.Id}: {user.Name}, {user.Age} лет");
}
}
}
}
Как видите, Dapper автоматически маппит результаты SQL-запроса на объекты C#. Метод Query<T> — это основной метод для выполнения SELECT запросов.
Основные методы/функции Dapper
Dapper предоставляет набор методов-расширений для интерфейса IDbConnection. Рассмотрим 10 ключевых методов.
1. Query<T>
Что делает: Выполняет SQL-запрос и возвращает коллекцию объектов указанного типа T. Используется для SELECT запросов, возвращающих несколько строк.
Параметры:
sql(string) — SQL-запрос.param(object, необязательно) — анонимный объект с параметрами запроса.transaction(IDbTransaction, необязательно) — транзакция.buffered(bool, по умолчанию true) — буферизировать ли результат в памяти. Если false, возвращается итератор, читающий данные по мере обхода.commandTimeout(int?, необязательно) — таймаут выполнения команды.commandType(CommandType?, необязательно) — тип команды (Text, StoredProcedure).
Пример:
var users = connection.Query<User>("SELECT * FROM Users WHERE Age > @age", new { age = 18 });
2. QueryFirst<T>
Что делает: Выполняет SQL-запрос и возвращает первый элемент результата. Если результат пуст, выбрасывает исключение InvalidOperationException.
Параметры: Аналогичны Query<T>.
Пример:
var user = connection.QueryFirst<User>("SELECT * FROM Users WHERE Id = @id", new { id = 1 });
3. QueryFirstOrDefault<T>
Что делает: Выполняет SQL-запрос и возвращает первый элемент результата. Если результат пуст, возвращает default(T) (null для ссылочных типов).
Параметры: Аналогичны Query<T>.
Пример:
var user = connection.QueryFirstOrDefault<User>("SELECT * FROM Users WHERE Id = @id", new { id = 999 });
4. QuerySingle<T>
Что делает: Выполняет SQL-запрос и возвращает единственный элемент. Если результат содержит более одной строки, выбрасывает исключение. Если результат пуст — также исключение.
Параметры: Аналогичны Query<T>.
Пример:
var user = connection.QuerySingle<User>("SELECT * FROM Users WHERE Id = @id", new { id = 1 });
5. QuerySingleOrDefault<T>
Что делает: Выполняет SQL-запрос и возвращает единственный элемент. Если результат пуст, возвращает default(T). Если строк больше одной — исключение.
Параметры: Аналогичны Query<T>.
Пример:
var user = connection.QuerySingleOrDefault<User>("SELECT * FROM Users WHERE Id = @id", new { id = 999 });
6. Execute
Что делает: Выполняет SQL-команду, не возвращающую строки (INSERT, UPDATE, DELETE, DDL). Возвращает количество затронутых строк.
Параметры:
sql(string) — SQL-запрос.param(object, необязательно) — параметры.transaction(IDbTransaction, необязательно) — транзакция.commandTimeout(int?, необязательно) — таймаут.commandType(CommandType?, необязательно) — тип команды.
Пример:
int affected = connection.Execute("UPDATE Users SET Age = @age WHERE Id = @id", new { age = 25, id = 1 });
7. ExecuteScalar<T>
Что делает: Выполняет SQL-запрос и возвращает первый столбец первой строки результата в виде значения указанного типа. Идеально для запросов с агрегатными функциями (COUNT, SUM).
Параметры: Аналогичны Execute.
Пример:
int count = connection.ExecuteScalar<int>("SELECT COUNT(*) FROM Users");
8. QueryMultiple
Что делает: Выполняет SQL-запрос, который возвращает несколько результирующих наборов (например, при использовании нескольких SELECT или хранимой процедуры). Возвращает объект SqlMapper.GridReader, из которого можно последовательно читать наборы.
Параметры: Аналогичны Query<T>.
Пример:
using (var multi = connection.QueryMultiple("SELECT * FROM Users; SELECT * FROM Orders"))
{
var users = multi.Read<User>();
var orders = multi.Read<Order>();
}
9. ExecuteReader
Что делает: Выполняет SQL-запрос и возвращает IDataReader для низкоуровневого чтения данных. Полезно, когда нужен полный контроль над процессом чтения.
Параметры: Аналогичны Execute.
Пример:
using (var reader = connection.ExecuteReader("SELECT Id, Name FROM Users"))
{
while (reader.Read())
{
Console.WriteLine($"{reader["Id"]}: {reader["Name"]}");
}
}
10. ExecuteAsync / QueryAsync и асинхронные версии
Что делает: Все перечисленные методы имеют асинхронные версии с суффиксом Async. Они возвращают Task или Task<T> и поддерживают CancellationToken.
Параметры: Аналогичны синхронным версиям + cancellationToken (необязательно).
Пример:
var users = await connection.QueryAsync<User>("SELECT * FROM Users");
Реальный практический пример использования
Рассмотрим реальный сценарий: интернет-магазин с таблицами Products и Categories. Нам нужно получить список товаров с названиями категорий, а также добавить новый товар.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
public string CategoryName { get; set; } // из JOIN
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ProductRepository
{
private readonly string _connectionString;
public ProductRepository(string connectionString)
{
_connectionString = connectionString;
}
// Получение всех товаров с категориями
public async Task<IEnumerable<Product>> GetAllProductsAsync()
{
const string sql = @"
SELECT p.Id, p.Name, p.Price, p.CategoryId, c.Name AS CategoryName
FROM Products p
INNER JOIN Categories c ON p.CategoryId = c.Id";
using (var connection = new SqlConnection(_connectionString))
{
return await connection.QueryAsync<Product>(sql);
}
}
// Добавление нового товара
public async Task<int> AddProductAsync(Product product)
{
const string sql = @"
INSERT INTO Products (Name, Price, CategoryId)
VALUES (@Name, @Price, @CategoryId);
SELECT CAST(SCOPE_IDENTITY() AS INT);";
using (var connection = new SqlConnection(_connectionString))
{
return await connection.QuerySingleAsync<int>(sql, product);
}
}
// Получение товаров с пагинацией
public async Task<IEnumerable<Product>> GetProductsPagedAsync(int pageNumber, int pageSize)
{
const string sql = @"
SELECT p.Id, p.Name, p.Price, p.CategoryId, c.Name AS CategoryName
FROM Products p
INNER JOIN Categories c