🚫 ءDbContext Thread-Safe نیست: اجرای موازی Queryهای EF Core به روش درست
ما همه یک بار آن endpoint را ساختهایم.
همان Endpoint معروفی که برای Executive Dashboard یا User Summary استفاده میشود. جایی که باید چندین مجموعه دادهٔ کاملاً نامربوط را بگیری تا یک تصویر کامل به کاربر نشان بدهی.
مثلاً:
• ۵۰ سفارش آخر
• لاگهای سلامت سیستم
• تنظیمات پروفایل کاربر
• تعداد اعلانها
و معمولاً کد را اینگونه مینویسیم:
var orders = await GetRecentOrdersAsync(userId);
var logs = await GetSystemLogsAsync();
var stats = await GetUserStatsAsync(userId);
return new DashboardDto(orders, logs, stats);
این کد کار میکند. تمیز است. خواناست. اما یک مشکل جدی وجود دارد. ⚠️
اگر:
🔹️GetRecentOrdersAsync = 300ms
🔹️GetSystemLogsAsync = 400ms
🔹️GetUserStatsAsync = 300ms
کاربر باید ۱ ثانیه کامل منتظر بماند (۳۰۰ + ۴۰۰ + ۳۰۰).
در سیستمهای توزیعشده، Latency تجربهٔ کاربری را نابود میکند.
و چون این دادهها از هم مستقل هستند، باید بتوانیم آنها را به صورت موازی اجرا کنیم.
اگر این کار را بکنیم، طول کل درخواست = کندترین Query (۴۰۰ms)
یعنی ۶۰٪ بهبود عملکرد فقط با تغییر نحوهٔ اجرای Queryها. 🔥
اما اگر با Entity Framework Core به شکل ساده بخواهی آنها را موازی کنی…
اپلیکیشن Crash میکند.
💥 وعدهٔ دروغین Task.WhenAll
رایجترین اشتباه این است که کد را برداری، آن را در چند Task بیندازی و منتظر بمانی:
// ❌ DO NOT DO THIS
public async Task<DashboardData> GetDashboardData(int userId)
{
var ordersTask = _repository.GetOrdersAsync(userId);
var logsTask = _repository.GetLogsAsync();
var statsTask = _repository.GetStatsAsync(userId);
await Task.WhenAll(ordersTask, logsTask, statsTask); // BOOM 💥
return new DashboardData(ordersTask.Result, logsTask.Result, statsTask.Result);
}
اگر این را اجرا کنی، بلافاصله با این Exception روبرو میشوی:
A second operation started on this context before a previous operation completed.
This is usually caused by different threads using the same instance of DbContext...
چرا این اتفاق میافتد؟ 🤔
❗️چرا DbContext Thread-Safe نیست؟
زیرا DbContext در EF Core stateful است و برای "یک Unit of Work" طراحی شده.
دلایل اصلی:
1️⃣ Change Tracker
ءDbContext وضعیت Entityهایی که بارگذاری شدهاند را نگه میدارد.
دو Thread همزمان نمیتوانند این وضعیت را تغییر دهند.
2️⃣ Single Database Connection
هر DbContext معمولاً یک Connection دارد.
پروتکل پایگاهداده (TCP Stream برای PostgreSQL یا SQL Server) اجازهٔ اجرای همزمان چند Query روی یک Connection را نمیدهد.
3️⃣ ءRace Condition و خراب شدن State داخلی
اگر دو Query همزمان اجرا شوند، DbContext نمیداند کدام نتیجه مربوط به کدام Query است.
بنابراین EF Core برای جلوگیری از فساد داده، Exception میاندازد.
🧩 پس یک تناقض داریم:
میخواهیم Queryها را موازی اجرا کنیم
اما EF Core ما را مجبور میکند پیاپی اجرا کنیم.
در ادامهٔ مقاله راهحلهای درست معرفی میشود…