Avatar ·

Как выполнить LINQ-запрос к DataTable в C# .NET 3.5?

Всем привет! Я начинающий C#-разработчик, работаю с .NET Framework 3.5. У меня есть DataTable, заполненный данными из базы. Мне нужно отфильтровать строки по условию и получить список объектов. Например, есть таблица Employees с колонками ID, Name, Department, Salary. Хочу выбрать всех сотрудников из отдела "IT" с зарплатой выше 50000.

Пробовал писать так:

DataTable dt = GetEmployeesDataTable();
var result = from row in dt.Rows
             where row["Department"] == "IT" && (int)row["Salary"] > 50000
             select row;

Но получаю ошибку компиляции: "Could not find an implementation of the query pattern for source type 'DataRowCollection'. 'Where' not found." Понимаю, что DataTable.Rows — это не коллекция, поддерживающая LINQ. Как правильно написать запрос? Нужно ли преобразовывать DataTable во что-то другое? Буду благодарен за примеры кода.

Также интересует: можно ли использовать анонимные типы для проекции, чтобы сразу получить список Employee? И как обрабатывать DBNull в колонках? Заранее спасибо!

<<>>

Решение: LINQ к DataTable через AsEnumerable()

Проблема в том, что DataTable.Rows возвращает DataRowCollection, который не реализует IEnumerable<T>, а только IEnumerable. Для LINQ-запросов нужен типизированный перечислитель. В .NET 3.5 это решается с помощью метода расширения AsEnumerable(), который находится в пространстве имен System.Data (сборка System.Data.DataSetExtensions.dll).

Шаг 1: Подключите пространство имен

using System.Data; // для AsEnumerable() и Field<T>()

Шаг 2: Используйте AsEnumerable() для DataTable

DataTable dt = GetEmployeesDataTable();

var query = from row in dt.AsEnumerable()
            where row.Field<string>("Department") == "IT" 
                  && row.Field<int>("Salary") > 50000
            select new
            {
                ID = row.Field<int>("ID"),
                Name = row.Field<string>("Name"),
                Department = row.Field<string>("Department"),
                Salary = row.Field<int>("Salary")
            };

foreach (var emp in query)
{
    Console.WriteLine($"{emp.Name} - {emp.Salary}");
}

Объяснение ключевых моментов:

  • AsEnumerable() — возвращает IEnumerable<DataRow>, что позволяет использовать LINQ.
  • Field<T>("columnName") — типизированный доступ к колонке, который автоматически обрабатывает DBNull (возвращает default(T) для null).
  • Вместо row["Department"] используйте row.Field<string>("Department") — это безопаснее и читаемее.

Шаг 3: Преобразование в список объектов Employee

Если у вас есть класс Employee:

public class Employee
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Department { get; set; }
    public int Salary { get; set; }
}

// Запрос с проекцией в Employee
List<Employee> employees = (from row in dt.AsEnumerable()
                            where row.Field<string>("Department") == "IT" 
                                  && row.Field<int>("Salary") > 50000
                            select new Employee
                            {
                                ID = row.Field<int>("ID"),
                                Name = row.Field<string>("Name"),
                                Department = row.Field<string>("Department"),
                                Salary = row.Field<int>("Salary")
                            }).ToList();

Обработка DBNull

Если колонка может содержать DBNull, используйте Field<T?> для nullable-типов:

var query = dt.AsEnumerable()
              .Where(row => row.Field<string>("Department") == "IT")
              .Select(row => new 
              {
                  Name = row.Field<string>("Name"),
                  Bonus = row.Field<int?>("Bonus") ?? 0 // если null, то 0
              });

Совет для .NET 3.5

Убедитесь, что в проекте добавлена ссылка на System.Data.DataSetExtensions.dll (обычно она есть по умолчанию). Если нет — добавьте через меню Project → Add Reference.

<<>>

Альтернативный подход: использование CopyToDataTable() и лямбда-синтаксис

Если вам нужно не только отфильтровать, но и получить новый DataTable с результатами, используйте метод CopyToDataTable(). Он доступен после AsEnumerable() и создаёт копию структуры таблицы с отфильтрованными данными.

DataTable dt = GetEmployeesDataTable();

var filteredRows = dt.AsEnumerable()
                     .Where(row => row.Field<string>("Department") == "IT" 
                                   && row.Field<int>("Salary") > 50000);

DataTable resultTable = filteredRows.CopyToDataTable();

Когда это полезно:

  • Нужно сохранить структуру исходной таблицы (колонки, типы).
  • Результат требуется передать в другой метод, ожидающий DataTable.
  • Вы хотите применить дальнейшие операции (например, сортировку) через DataView.

Важно: CopyToDataTable() работает только с перечислителем, полученным из DataTable. Если вы попытаетесь вызвать его на произвольном IEnumerable, получите исключение.

Также можно комбинировать лямбда-синтаксис и query-синтаксис — выбирайте тот, который понятнее вашей команде. Оба подхода равнозначны по производительности.