Ошибка «Уже открыт DataReader, связанный с этой командой» в Entity Framework
Причина ошибки
Ошибка возникает из-за того, что по умолчанию 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. Старайтесь минимизировать количество обращений к базе данных в рамках одного контекста.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги
C# vs Python для бэкенда: сравнение и выбор в 2024
Оптимизация производительности JavaScript приложений: советы и примеры