Avatar ·

Ошибка «Уже открыт DataReader, связанный с этой командой» в Entity Framework

Всем привет! Я новичок в C# и Entity Framework. Пишу простое приложение для интернет-магазина, и столкнулся с очень странной ошибкой. У меня есть метод, который получает список заказов пользователя и для каждого заказа выводит информацию о товарах. Вот мой код: ```csharp using (var context = new MyDbContext()) { var orders = context.Orders.Where(o => o.UserId == userId).ToList(); foreach (var order in orders) { Console.WriteLine($"Заказ #{order.Id}"); // Пытаюсь получить товары для каждого заказа var products = context.Products .Where(p => p.OrderId == order.Id) .ToList(); foreach (var product in products) { Console.WriteLine($" - {product.Name}: {product.Price} руб."); } } } ``` На строчке `var products = context.Products...` я получаю ошибку: > "There is already an open DataReader associated with this Command which must be closed first" Я понимаю, что это как-то связано с тем, что я пытаюсь использовать одно и то же соединение для двух запросов одновременно. Но я же использую `ToList()`, который должен загрузить данные сразу! Почему возникает эта ошибка и как ее правильно исправить? Использую Entity Framework 4, .NET Framework 4.5. Заранее спасибо за помощь! <<>>

Причина ошибки

Ошибка возникает из-за того, что по умолчанию Entity Framework использует MARS (Multiple Active Result Sets) отключенным. Когда вы вызываете ToList() для первого запроса, DataReader остается открытым, пока вы итерируетесь по коллекции orders. В этот момент вы пытаетесь выполнить второй запрос к той же базе данных, что приводит к конфликту — один DataReader уже открыт, а второй открыть нельзя без закрытия первого.

Хотя вы вызываете ToList(), Entity Framework использует отложенную загрузку (lazy loading) для навигационных свойств. В вашем случае, если у сущности Order есть навигационное свойство Products, то при обращении к нему внутри цикла EF попытается выполнить дополнительный запрос, что и вызывает ошибку.

Решение 1: Жадная загрузка (Eager Loading) с Include

Самый простой и эффективный способ — загрузить все связанные данные одним запросом с помощью метода Include:

using (var context = new MyDbContext())
{
    var orders = context.Orders
        .Include(o => o.Products)  // Загружаем товары вместе с заказами
        .Where(o => o.UserId == userId)
        .ToList();
    
    foreach (var order in orders)
    {
        Console.WriteLine($"Заказ #{order.Id}");
        
        // Products уже загружены, дополнительного запроса не будет
        foreach (var product in order.Products)
        {
            Console.WriteLine($" - {product.Name}: {product.Price} руб.");
        }
    }
}

Решение 2: Явная загрузка (Explicit Loading)

Если вам нужно загружать данные выборочно, используйте метод Collection.Load():

using (var context = new MyDbContext())
{
    var orders = context.Orders
        .Where(o => o.UserId == userId)
        .ToList();
    
    foreach (var order in orders)
    {
        Console.WriteLine($"Заказ #{order.Id}");
        
        // Явно загружаем товары для текущего заказа
        context.Entry(order).Collection(o => o.Products).Load();
        
        foreach (var product in order.Products)
        {
            Console.WriteLine($" - {product.Name}: {product.Price} руб.");
        }
    }
}

Решение 3: Включение MARS в строке подключения

Менее предпочтительный вариант — включить поддержку множественных активных результирующих наборов. Добавьте MultipleActiveResultSets=True в строку подключения:

<connectionStrings>
    <add name="MyDbContext" 
         connectionString="Server=.;Database=MyDb;Integrated Security=True;MultipleActiveResultSets=True" 
         providerName="System.Data.SqlClient"/>
</connectionStrings>

Однако это может привести к проблемам с производительностью и блокировками, поэтому используйте этот метод с осторожностью.

Рекомендации

  • Всегда используйте Include для загрузки связанных данных, если знаете, что они понадобятся
  • Отключайте ленивую загрузку (context.Configuration.LazyLoadingEnabled = false;) в конструкторе контекста, чтобы избежать случайных запросов
  • Используйте проекции с Select, если нужно только несколько полей — это уменьшит объем передаваемых данных
<<>>

Альтернативный подход: Использование проекций и Select

Если вам не нужны полные сущности, а только определенные поля, можно выполнить один запрос с проекцией. Это часто быстрее и безопаснее:

using (var context = new MyDbContext())
{
    var orderProducts = context.Orders
        .Where(o => o.UserId == userId)
        .Select(o => new 
        {
            OrderId = o.Id,
            Products = o.Products.Select(p => new 
            { 
                p.Name, 
                p.Price 
            })
        })
        .ToList();
    
    foreach (var item in orderProducts)
    {
        Console.WriteLine($"Заказ #{item.OrderId}");
        foreach (var product in item.Products)
        {
            Console.WriteLine($" - {product.Name}: {product.Price} руб.");
        }
    }
}

Дополнительный совет: Использование ToDictionary

Если данные нужно часто переиспользовать, загрузите все в словарь за один раз:

var ordersDict = context.Orders
    .Where(o => o.UserId == userId)
    .Include(o => o.Products)
    .ToDictionary(o => o.Id);

Этот подход особенно полезен, когда вы работаете с большими объемами данных и хотите избежать множественных запросов к базе. Главное правило: один запрос — один DataReader. Старайтесь минимизировать количество обращений к базе данных в рамках одного контекста.