در دنیای داتنت، دسترسی به پایگاههای داده SQL Server و Azure SQL از طریق ارائهدهنده ADO.NET یعنی Microsoft.Data.SqlClient انجام میشود. این کتابخانه یکی از ابزارهای کلیدی برای تعامل با دادهها است و در وبسرویسها، برنامههای دسکتاپ و حتی برنامههای مبتنی بر Entity Framework Core (EF Core) استفاده میشود. یکی از مشکلات دیرینه در این کتابخانه، کندی شدید خواندن دادههای بزرگ (مانند دادههای باینری یا متنی بزرگ) به صورت asynchronous بود. اما اکنون، با انتشار نسخه پیشنمایش ۷.۰ (preview ۲) از Microsoft.Data.SqlClient، یک ویژگی جدید معرفی شده که میتواند سرعت این عملیات را تا ۱۸۰ درصد افزایش دهد. در این مقاله، به بررسی زمینه این مشکل، راهحل ارائهشده، نحوه فعالسازی آن، نتایج بنچمارک و نکات تکمیلی میپردازیم تا توسعهدهندگان بتوانند از این بهبود عملکرد بهره ببرند.
زمینه و مشکل اصلی
Microsoft.Data.SqlClient جایگزین مدرن System.Data.SqlClient است و برای اتصال به SQL Server طراحی شده. عملیات asynchronous در .NET بسیار مهم هستند، زیرا اجازه میدهند برنامهها بدون مسدود کردن thread اصلی، عملیات ورودی/خروجی (مانند خواندن از پایگاه داده) را انجام دهند. این ویژگی به ویژه در برنامههای وب و سرویسهای ابری مفید است، جایی که scalability و پاسخدهی سریع حیاتی است.
یکی از مسائل پُررای در مخزن GitHub این پروژه (با بیش از صدها رای مثبت)، کندی خواندن دادههای بزرگ asynchronous بود. برای مثال، خواندن یک فیلد VARBINARY(MAX) با اندازه ۱۰ مگابایت به صورت async ممکن بود تا ۵ ثانیه طول بکشد، در حالی که همان عملیات به صورت synchronous تنها ۲۰ میلیثانیه زمان میبرد. این مشکل بیش از ۵ سال پیش توسط جامعه توسعهدهندگان گزارش شد و ریشه آن در نحوه مدیریت پکتهای داده در پروتکل TDS (Tabular Data Stream) بود. در نسخههای قدیمی، عملیات async برای دادههای بزرگ (LOB - Large Object) از روشهای ناکارآمد استفاده میکرد، مانند خواندن تکپکتی یا ایجاد تاخیرهای غیرضروری در شبکه، که منجر به عملکرد ضعیف میشد.
جامعه توسعهدهندگان، از جمله مشارکتکنندهای به نام Wraith2، سالها برای حل این مشکل تلاش کردند. پس از چندین تلاش ناموفق (از جمله در نسخه ۶.۱.۰ که به دلیل رگرسیونها پس گرفته شد)، این اصلاح در نهایت در نسخه ۷.۰ preview ۲ (منتشر شده در ۱۷ اکتبر ۲۰۲۵) ادغام شد. این نسخه از تکنیکهایی مانند packet multiplexing (چندگانهسازی پکتها) برای بهبود عملکرد استفاده میکند، که اجازه میدهد دادههای بزرگ به طور موازی و کارآمد خوانده شوند.
راهحل و نحوه فعالسازی
برای بهرهبرداری از این بهبود، باید نسخه جدید کتابخانه را نصب کنید و دو سوئیچ AppContext را تنظیم نمایید. این سوئیچها برای حفظ سازگاری با رفتار قدیمی (backward compatibility) اضافه شدهاند و ویژگی جدید را opt-in میکنند. در آینده ممکن است این رفتار به صورت پیشفرض فعال شود، اما فعلاً نیاز به تنظیم دستی دارد.
گامهای فعالسازی:
۱. بهروزرسانی بسته NuGet: در فایل پروژه (.csproj) خود، اشاره به نسخه جدید را اضافه یا بهروزرسانی کنید:
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0-preview2.25289.6" />
این نسخه پیشنمایش است و برای تولید ممکن است نیاز به نسخه پایدار آینده داشته باشید. آخرین نسخه پایدار فعلی (۶.۱.۲، منتشر شده در ۸ اکتبر ۲۰۲۵) هنوز این ویژگی را به طور کامل ندارد، اما نسخههای پیشنمایش ۷.۰ آن را پشتیبانی میکنند.
۲. تنظیم سوئیچها: در ابتدای برنامه خود (مثلاً در فایل Program.cs)، این دو خط را اضافه کنید:
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour", false);
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni", false);این سوئیچها رفتار جدید را فعال میکنند و اجازه میدهند از بهبودهای multi-packet استفاده شود.
مثال کد برای خواندن داده بزرگ async:
برای نشان دادن کاربرد، فرض کنید میخواهید یک فیلد باینری بزرگ را از جدول بخوانید:
using Microsoft.Data.SqlClient;
using System.IO;
async Task<byte[]> ReadLargeBinaryAsync(string connectionString, int id)
{
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
using var command = new SqlCommand("SELECT LargeBinary FROM MyTable WHERE Id = @Id", connection);
command.Parameters.AddWithValue("@Id", id);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
using var memoryStream = new MemoryStream();
await reader.GetStream(0).CopyToAsync(memoryStream);
return memoryStream.ToArray();
}
return null;
}با فعالسازی سوئیچها، این کد بسیار سریعتر اجرا میشود.
نتایج بنچمارک و مقایسه
بنچمارکها نشاندهنده بهبود چشمگیر هستند. در نسخههای قدیمی (مانند ۵.۱.۶ که توسط EF Core ۹ استفاده میشود)، خواندن async یک داده بزرگ ممکن است ۱.۷ ثانیه طول بکشد، در حالی که synchronous تنها ۰.۰۳ ثانیه زمان میبرد. با فعالسازی ویژگی جدید:
- Async: از ۱,۷۱۳ میلیثانیه به حدود ۶۰ میلیثانیه کاهش مییابد (افزایش سرعت ۱۸۰ درصدی نسبت به نسخههای قدیمی).
- تخصیص حافظه: کاهش از ۳۰.۶۷ مگابایت به سطوح پایینتر.
- garbage collection: بهبود در نسلهای Gen0، Gen1 و Gen2.
این اعداد از بنچمارکهای ساده به دست آمدهاند، اما در سناریوهای واقعی (مانند خواندن تصاویر، فایلها یا متنهای طولانی از Azure SQL)، تفاوت حتی بیشتر است. توجه کنید که اگر از EF Core استفاده میکنید، باید بسته SqlClient را به طور مستقل بهروزرسانی کنید، زیرا EF Core به نسخه قدیمیتر وابسته است.
نکات تکمیلی و هشدارها
- تأثیر بر EF Core: اگر از EF Core (نسخه ۸ یا ۹) استفاده میکنید، این بهبود مستقیماً اعمال میشود، زیرا EF Core از SqlClient استفاده میکند. اما تست کنید تا مطمئن شوید هیچ رگرسیونی رخ نمیدهد.
- موارد استفاده: این ویژگی برای برنامههایی که با دادههای LOB بزرگ سروکار دارند (مانند ذخیرهسازی فایلها، تصاویر یا JSONهای بزرگ) ایدهآل است. در عملیات کوچک، تفاوت کمتری مشاهده میشود.
- ریسکهای پیشنمایش: نسخه ۷.۰ هنوز preview است، پس برای محیطهای تولید صبر کنید تا نسخه پایدار منتشر شود. در نسخه ۶.۱.۰، رگرسیونهایی مانند corruption داده یا خطاهای نادر وجود داشت که منجر به delist شدن آن شد.
- جایگزینها: اگر نمیتوانید بهروزرسانی کنید، از عملیات synchronous استفاده کنید یا دادهها را به قطعات کوچکتر تقسیم نمایید، اما این روشها scalability را کاهش میدهند.
- آینده: توسعهدهندگان پروژه قول دادهاند این ویژگی را در نسخههای آینده به صورت پیشفرض فعال کنند. همچنین، بهبودهای دیگری مانند پشتیبانی از دادههای vector در SQL Server در حال اضافه شدن است.
نتیجهگیری
این بهبود در Microsoft.Data.SqlClient یک گام بزرگ برای توسعهدهندگان .NET است که با دادههای بزرگ کار میکنند. با فعالسازی ساده دو سوئیچ، میتوانید عملکرد async را به سطح synchronous نزدیک کنید و برنامههای خود را کارآمدتر سازید. اگر توسعهدهنده هستید، حتماً این نسخه را تست کنید و بازخوردهای خود را در GitHub پروژه به اشتراک بگذارید.