Работа с DataTable через LINQ: от новичка до мастера за 20 минут
Представьте ситуацию: вы пишете приложение на C#, загружаете данные из базы в DataTable, а затем… вам нужно отфильтровать строки по сложному условию, сгруппировать по дате или объединить две таблицы. Старый добрый DataTable.Select("...") с его строковыми фильтрами ломается на втором условии, а циклы foreach превращают код в спагетти.
Знакомо? Тогда добро пожаловать в мир LINQ запросов к DataTable. Это — ваш супер-инструмент, который превращает работу с табличными данными в элегантный, читаемый и мощный код. В этом руководстве мы разберём всё: от базовых Where до продвинутых Join и GroupBy. Поехали!
Что такое LINQ to DataSet и зачем он нужен?
LINQ (Language Integrated Query) — это встроенный в C# язык запросов. Его главная фишка: вы пишете запросы к любым данным (массивы, списки, XML, базы данных) одинаково — с помощью знакомых операторов from, where, select.
Но с DataTable есть нюанс: напрямую LINQ к нему не применить. DataTable не реализует интерфейс IEnumerable<T>. Решение — метод AsEnumerable(), который превращает таблицу в коллекцию объектов DataRow. Именно с ней и работает LINQ.
Плюсы подхода:
- ✅ Типобезопасность: компилятор ловит ошибки в названиях колонок
- ✅ Читаемость: код похож на SQL, но остаётся C#
- ✅ Мощь: фильтрация, проекция, группировка, сортировка — одной строкой
- ✅ Производительность: отложенное выполнение (не загружает лишнего)
Давайте сразу к делу. Установите в проекте ссылку на System.Data.DataSetExtensions (обычно уже есть) — и вперёд.
Подготовка данных: наша тестовая таблица
Чтобы не быть голословными, создадим DataTable с данными о товарах. Код ниже — наша база для всех примеров:
// 1. Создаём таблицу с колонками
DataTable products = new DataTable("Products");
products.Columns.Add("Id", typeof(int));
products.Columns.Add("Name", typeof(string));
products.Columns.Add("Category", typeof(string));
products.Columns.Add("Price", typeof(decimal));
products.Columns.Add("DateAdded", typeof(DateTime));
// 2. Добавляем строки
products.Rows.Add(1, "Ноутбук ASUS", "Электроника", 55000, DateTime.Now.AddDays(-10));
products.Rows.Add(2, "Мышь Logitech", "Электроника", 2500, DateTime.Now.AddDays(-5));
products.Rows.Add(3, "Книга 'C# in Depth'", "Книги", 1800, DateTime.Now.AddDays(-2));
products.Rows.Add(4, "Наушники Sony", "Электроника", 12000, DateTime.Now.AddDays(-1));
products.Rows.Add(5, "Блокнот Moleskine", "Канцелярия", 1200, DateTime.Now.AddDays(-20));
products.Rows.Add(6, "Ручка Parker", "Канцелярия", 3500, DateTime.Now.AddDays(-15));
products.Rows.Add(7, "Монитор Dell", "Электроника", 32000, DateTime.Now.AddDays(-30));
Теперь у нас есть 7 товаров. Пора применить магию LINQ.
Базовые LINQ запросы к DataTable
1. Фильтрация (Where) — как SQL WHERE
Допустим, нужно выбрать все товары дороже 10 000 рублей. Без LINQ вы бы писали цикл с проверкой row["Price"]. С LINQ это выглядит так:
var expensiveProducts = products.AsEnumerable()
.Where(row => row.Field<decimal>("Price") > 10000);
foreach (var row in expensiveProducts)
{
Console.WriteLine($"{row["Name"]} - {row["Price"]} руб.");
}
// Вывод: Ноутбук ASUS - 55000, Наушники Sony - 12000, Монитор Dell - 32000
Важный момент: используйте метод Field<T>("ColumnName") вместо прямого приведения (decimal)row["Price"]. Это безопаснее: если колонка DBNull, Field вернёт null (для ссылочных типов) или выбросит исключение (для значимых). Всегда используйте Field!
2. Проекция (Select) — выбираем нужные колонки
Хотите получить только названия и цены, да ещё и с налогом? Легко:
var productInfos = products.AsEnumerable()
.Select(row => new
{
Name = row.Field<string>("Name"),
PriceWithTax = row.Field<decimal>("Price") * 1.2m
});
foreach (var item in productInfos)
Console.WriteLine($"{item.Name}: {item.PriceWithTax:F2} руб.");
Обратите внимание: мы создаём анонимный тип на лету. Это удобно, когда не нужно возвращать целую строку DataRow.
3. Сортировка (OrderBy, ThenBy)
Отсортируем товары по цене по возрастанию, а затем по дате добавления:
var sortedProducts = products.AsEnumerable()
.OrderBy(row => row.Field<decimal>("Price"))
.ThenByDescending(row => row.Field<DateTime>("DateAdded"));
// Для убывания: OrderByDescending()
Продвинутые техники: группировка и объединение
4. Группировка (GroupBy) — считаем статистику по категориям
Как быстро узнать среднюю цену товара по каждой категории? SQL-запрос GROUP BY в C#:
var categoryStats = products.AsEnumerable()
.GroupBy(row => row.Field<string>("Category"))
.Select(group => new
{
Category = group.Key,
Count = group.Count(),
AvgPrice = group.Average(r => r.Field<decimal>("Price")),
TotalPrice = group.Sum(r => r.Field<decimal>("Price"))
});
foreach (var stat in categoryStats)
{
Console.WriteLine($"{stat.Category}: {stat.Count} товаров, " +
$"средняя цена {stat.AvgPrice:F2} руб.");
}
// Вывод:
// Электроника: 4 товаров, средняя цена 25375,00 руб.
// Книги: 1 товаров, средняя цена 1800,00 руб.
// Канцелярия: 2 товаров, средняя цена 2350,00 руб.
Совет: GroupBy — один из самых полезных операторов. С его помощью можно делать любые агрегации: Min, Max, Average,