عنوان:

‫ارتقا به NET 10. و EF Core 10: تغییرات رفتار زمان اجرا که در زمان کامپایل پنهان می‌مانند


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۴ ۱۰:۲۰
آدرس: www.dntips.ir
چکیده: مهاجرت به نسخه‌های جدید دات‌نت معمولاً با بررسی خطاهای زمان کامپایل (Compile-time Errors) و APIهای منسوخ‌شده آغاز می‌شود؛ با این حال، حساس‌ترین چالش‌ها مربوط به «تغییرات رفتاری زمان اجرا» (Runtime Behavioral Breaking Changes) هستند. در Entity Framework Core 10، تغییر بنیادینی در استراتژی ترجمه LINQ به SQL برای عملگر متداول Contains() اعمال شده است. در این نسخه، ترجمه پیش‌فرض از آرایه‌های مبتنی بر JSON (نظیر OPENJSON در SQL Server یا json_each در SQLite) به پارامترهای اسکالر تفکیک‌شده (Multiple Scalar Parameters) تغییر یافته است. این تغییر اگرچه در خروجی داده‌ها تفاوتی ایجاد نمی‌کند، اما ساختار متن SQL، شیوه‌ی نام‌گذاری پارامترها و رفتار حافظه موقت پلن‌های اجرایی دیتابیس (Query Plan Cache) را دگرگون می‌سازد. این مقاله به تحلیل دقیق این تغییر، اثرات آن بر کارایی و روش‌های کنترل یا بازگردانی رفتار قبلی می‌پردازد.

مقدمه
در معماری سیستم‌های مبتنی بر پایگاه داده رابطه ای، ترجمه عبارات پرس‌وجو از زبان شیءگرا به SQL نیازمند تعادلی دقیق میان خوانایی کوئری، کارایی پردازش در سمت دیتابیس و مدیریت کش پلن اجرایی است. در EF Core 8 و 9، تیم دات‌نت رویکردی را اتخاذ کرده بود که مجموعه‌های پاس‌داده‌شده به عملگر Contains() را در قالب یک آرایه متنی یا JSON فشرده به دیتابیس ارسال می‌کرد تا با یک پارامتر واحد، کوئری‌های با طول پارامتر متغیر پردازش شوند. با انتشار EF Core 10، الگوی ترجمه به صورت پیش‌فرض بازنگری شده است. این تغییر بدون هیچ‌گونه هشدار یا خطایی در زمان کامپایل اعمال می‌شود، اما می‌تواند در لایه‌های زیرین (نظیر رفتارهای کشینگ SQL Server، ابزارهای پایش عملکرد، تست‌های Snapshot و رهگیرهای DbCommand) اثرات غیرمنتظره‌ای بر جای بگذارد.

تحلیل فنی تغییرات در EF Core 10
تغییر در نحوه ترجمه عملگرContains()
فرض کنید یک متد ساده جهت واکشی اطلاعات بر اساس فهرستی از شناسه‌ها داریم:
var idList = new List<int> { 1, 3 };
var blogs = await db.Blogs
                    .Where(b => idList.Contains(b.Id))
                    .ToListAsync();
رفتار در EF Core 9:
در نسخه ۹، موتور تولید پرس‌وجو کل کالکشن را به عنوان یک رشته JSON سریالایز کرده و در قالب یک پارامتر واحد ارسال می‌کرد:
  • در SQL Server:
-- EF Core 9 (SQL Server)
SELECT [b].[Id], [b].[Name], [b].[Address_City], [b].[Address_Street]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (
    SELECT [value] 
    FROM OPENJSON(@__idList_0)
)
-- Parameters: @__idList_0='[1,3]'
  • در SQLite:
-- EF Core 9 (SQLite)
SELECT "b"."Id", "b"."Name", "b"."Tags", "b"."Address_City", "b"."Address_Street"
FROM "Blogs" AS "b"
WHERE "b"."Id" IN (
    SELECT "i"."value"
    FROM json_each(@__idList_0) AS "i"
)
-- Parameters: @__idList_0='[1,3]'

رفتار در EF Core 10:
در نسخه ۱۰، بدون تغییر در کد سی‌شارپ، ترجمه به صورت فهرست پارامترهای اسکالر مجزا انجام می‌شود:
  • در SQL Server:
-- EF Core 10 (SQL Server)
SELECT [b].[Id], [b].[Name], [b].[Address_City], [b].[Address_Street]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (@idList1, @idList2)
-- Parameters: @idList1=1, @idList2=3
  • در SQLite:
-- EF Core 10 (SQLite)
SELECT "b"."Id", "b"."Name", "b"."Tags", "b"."Address_City", "b"."Address_Street"
FROM "Blogs" AS "b"
WHERE "b"."Id" IN (@idList1, @idList2)
-- Parameters: @idList1='1', @idList2='3'

ساده‌سازی الگوی نام‌گذاری پارامترها (Parameter Renaming)
تغییر همراه دیگر در EF Core 10، اصلاح نحوه نام‌گذاری پارامترهای ارسالی است:
  • در گذشته: پیشوند __ همراه با اندیس متغیر اعمال می‌شد (مانند @__idList_0 یا @__city_0).
  • در EF Core 10: نام‌گذاری تمیزتر شده و مستقیماً برگرفته از نام متغیر در کد سی‌شارپ است (مانند @idList1, @idList2 یا @city).

نکته حائز اهمیت: اگرچه این تغییر پایگاه‌داده را تحت تأثیر منفی قرار نمی‌دهد، اما دو دسته از پیاده‌سازی‌ها را بلافاصله با خطا مواجه می‌کند:
  • تست‌های مبتنی بر Snapshot: تست‌های واحد یا یکپارچه‌سازی که متن کوئری تولیدشده توسط EF Core را با رشته‌های ثابت مقایسه می‌کنند.
  • اینترسپتورها (Interceptors): کدهایی که در DbCommandInterceptor با عبارات باقاعده (Regex) یا جستجوی متنی روی DbCommand.CommandText به دنبال پارامترهایی با پیشوند __ می‌گشتند.

اثرات بر عملکرد و حافظه موقت پلن‌ها (Plan Cache Dynamics)
کلید شناسایی پلن‌های اجرایی در موتورهایی نظیر SQL Server، متن دقیق کوئری ارسالی (Exact SQL Text Hash) است. تغییر فرمت پرس‌وجو پیامدهای زیر را در بر دارد:

ویژگیرویکرد JSON (نسخه ۹)رویکرد پارامترهای اسکالر (نسخه ۱۰)
تعداد پلن‌های کش‌شدهبسیار پایین (یک پلن برای تمام اندازه‌های آرایه)بر اساس بازه‌های پارامترها کش ایجاد می‌شود
هزینه پردازش در دیتابیسهزینه اضافی جهت Parse و خواندن ساختار JSONپردازش مستقیم و سریع‌تر بدون Overhead جی‌سان
خطر Plan Cache Bloatنزدیک به صفردر صورت نبود مکانیزم باکت‌بندی (Parameter Bucketing)، خطر انباشت پلن‌ها وجود دارد
پیامد استقرار:
بلافاصله پس از انتشار و دیپلوی سرویس با EF Core 10، تمامی پلن‌های قبلی بی‌استفاده شده و در اولین فراخوانی کوئری‌ها، فرایند کامپایل پلن (Plan Compilation) رخ می‌دهد. این امر ممکن است در لحظات اولیه اجرای برنامه تحت بار کاری بالا، جهش لحظه‌ای در استفاده از پردازنده (CPU Spike) و افزایش تاخیر (Latency) ایجاد کند.

راهکارهای مدیریت و کنترل رفتار در سطح پروژه
اگر در سیستم خود وابستگی بالایی به الگوی قبلی دارید (مثلاً ارسال آرایه‌های بسیار بزرگ که تولید صدها پارامتر اسکالر برای آن‌ها بهینه نیست)، می‌توانید رفتار را در سطح سراسری یا در سطح کوئری کنترل کنید:
تنظیم سراسری (Global Configuration)
با استفاده از متد تنظیمات پرووایدر، رفتار مدل قدیمی را بازیابی کنید:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlServer(
        connectionString,
        sqlServerOptions => sqlServerOptions.UseParameterizedCollectionMode(
            ParameterTranslationMode.Parameter
        )
    );
}

اعمال در سطح یک کوئری خاص (Per-Query Control)
چنانچه تنها در چند نقطه حساس نیاز به ارسال پارامتر متمرکز دارید، نیازی به تغییر کل رفتارهای پروژه نیست و می‌توانید از EF.Parameter بهره بگیرید:
var blogs = await db.Blogs
                    .Where(b => EF.Parameter(idList).Contains(b.Id))
                    .ToListAsync();

نتیجه‌گیری
ارتقا به NET 10. و EF Core 10 از منظر سازگاری کدهای دات‌نت تغییری بدون شکست ظاهری (Compile-clean) به نظر می‌رسد، اما تفاوت‌های عمیقی در لایه دسترسی به داده‌ها ایجاد می‌کند. بازگشت به پارامترهای اسکالر در عملگر Contains()، بهینه‌سازی موثری برای سناریوهای معمول با تعداد آیتم‌های محدود است، اما نیازمند ارزیابی مجدد رفتار دیتابیس در ترافیک‌های بالا، بازبینی تست‌های Snapshot و دقت در اینترسپتورهای سفارشی است. پیشنهاد می‌شود پیش از استقرار نهایی در محیط عملیاتی، تست‌های بارگذاری با تمرکز بر مصرف حافظه موقت دیتابیس انجام شود.