مقدمه
در دنیای برنامهنویسی، سریالسازی (Serialization) دادهها به فرمت JSON یکی از عملیات رایج برای انتقال و ذخیرهسازی اطلاعات است. با این حال، هنگام سریالسازی اشیاء پیچیده، بهویژه آنهایی که دارای ارجاعات حلقوی (Circular References) هستند، چالشهایی به وجود میآید. ارجاعات حلقوی زمانی رخ میدهند که یک شیء به خودش یا به شیء دیگری که به طور غیرمستقیم به آن اشاره میکند، ارجاع دهد. در نسخههای اولیه System.Text.Json در چارچوب داتنت، مدیریت این ارجاعات در تولیدکنندههای منبع (Source Generators) پشتیبانی نمیشد و منجر به بروز خطا میگردید. با معرفی داتنت 10، قابلیتهای جدیدی به System.Text.Json اضافه شده است که امکان مدیریت ارجاعات حلقوی را از طریق تنظیمات ReferenceHandler فراهم میکند. این مقاله به بررسی این قابلیت جدید، نحوه کارکرد آن و چگونگی استفاده از آن در پروژههای داتنت میپردازد.
ارجاعات حلقوی چیست و چرا اهمیت دارد؟
ارجاعات حلقوی زمانی رخ میدهند که یک شیء در ساختار دادهای به خودش یا به شیء دیگری که به طور غیرمستقیم به آن ارجاع میدهد، اشاره کند. برای مثال، فرض کنید یک کلاس Employee داریم که شامل یک ویژگی Manager از همان نوع Employee است، و Manager نیز به یک Employee دیگر اشاره میکند که ممکن است همان شیء اولیه باشد. این ساختار میتواند در سریالسازی JSON مشکلساز شود، زیرا سریالساز (Serializer) ممکن است در یک حلقه بیپایان گیر کند و باعث بروز خطای سرریز پشته (Stack Overflow) یا استثنای سریالسازی شود.
در نسخههای قبلی System.Text.Json، تولیدکنندههای منبع که برای بهینهسازی عملکرد سریالسازی استفاده میشدند، از تنظیمات ReferenceHandler پشتیبانی نمیکردند. این محدودیت باعث میشد که سریالسازی اشیاء با ارجاعات حلقوی در چنین بسترهایی غیرممکن باشد و توسعهدهندگان مجبور به استفاده از روشهای جایگزین مانند Newtonsoft.Json یا غیرفعال کردن تولیدکنندههای منبع شوند. اما با داتنت 10، این مشکل برطرف شده و توسعهدهندگان میتوانند مستقیماً ارجاعات حلقوی را در تولیدکنندههای منبع مدیریت کنند، که این امر به بهبود عملکرد و کاهش پیچیدگی کد کمک میکند.
معرفی ReferenceHandler در داتنت 10
ویژگی ReferenceHandler در System.Text.Json امکان مدیریت ارجاعات در هنگام سریالسازی و دیسریالسازی (Deserialization) را فراهم میکند. دو گزینه اصلی برای ReferenceHandler وجود دارد: IgnoreCycles و Preserve. گزینه IgnoreCycles ارجاعات حلقوی را نادیده میگیرد و ویژگیهای حلقوی را با مقدار null جایگزین میکند، که باعث کاهش اندازه خروجی JSON میشود اما ممکن است اطلاعات رابطهای را از دست بدهد. در مقابل، گزینه Preserve ارجاعات را حفظ میکند و با افزودن متادادههایی مانند $id و $ref، ساختار شیء را به طور کامل در JSON بازنمایی میکند.
در داتنت 10، این قابلیت به تولیدکنندههای منبع گسترش یافته است. با استفاده از ویژگی [JsonSourceGenerationOptions]، توسعهدهندگان میتوانند ReferenceHandler را مستقیماً در زمینه تولید منبع (Source Generation Context) تنظیم کنند. این امکان به سریالساز اجازه میدهد تا به جای تولید استثنا، ارجاعات حلقوی را به درستی مدیریت کند. این بهبود نه تنها انعطافپذیری بیشتری به توسعهدهندگان میدهد، بلکه با حفظ مزایای عملکردی تولیدکنندههای منبع، نیاز به راهحلهای موقتی را کاهش میدهد.
مثال عملی: سریالسازی یک شیء خود-ارجاعی
برای درک بهتر نحوه کار این قابلیت، به یک مثال کد نگاه میکنیم که یک شیء خود-ارجاعی را سریالسازی میکند. فرض کنید کلاسی به نام SelfReference داریم که شامل یک ویژگی Me از همان نوع است و به خودش ارجاع میدهد. کد زیر نحوه استفاده از تولیدکنندههای منبع برای مدیریت این ارجاع حلقوی را نشان میدهد:
public static void MakeSelfRef()
{
var selfRef = new SelfReference();
selfRef.Me = selfRef;
string json = JsonSerializer.Serialize(selfRef, ContextWithPreserveReference.Default.SelfReference);
Console.WriteLine(json);
// Output: {"$id":"1","Me":{"$ref":"1"}}
}
[JsonSourceGenerationOptions(ReferenceHandler = JsonKnownReferenceHandler.Preserve)]
[JsonSerializable(typeof(SelfReference))]
internal partial class ContextWithPreserveReference : JsonSerializerContext
{
}
internal class SelfReference
{
public SelfReference Me { get; set; } = null!;
}در این مثال، شیء selfRef به خودش ارجاع میدهد. با تنظیم ReferenceHandler به Preserve در ویژگی [JsonSourceGenerationOptions]، سریالساز متادادههایی مانند $id و $ref را به خروجی JSON اضافه میکند. این متادادهها به سریالساز کمک میکنند تا ارجاعات حلقوی را شناسایی و حفظ کند، به طوری که خروجی JSON به جای تکرار بینهایت شیء، یک ارجاع به شیء اولیه را نشان میدهد. نتیجه این است که JSON تولیدشده هم دقیق است و هم از نظر اندازه بهینه.
مزایا و محدودیتهای استفاده از ReferenceHandler.Preserve
استفاده از ReferenceHandler.Preserve در تولیدکنندههای منبع مزایای متعددی دارد. اولاً، این روش امکان حفظ روابط پیچیده بین اشیاء را فراهم میکند، که در سناریوهای واقعی مانند مدلهای Entity Framework که اغلب شامل ارجاعات دوطرفه هستند، بسیار مفید است. ثانیاً، با استفاده از تولیدکنندههای منبع، عملکرد سریالسازی بهبود مییابد، زیرا کد سریالسازی در زمان کامپایل تولید میشود و نیاز به بازتاب (Reflection) در زمان اجرا را از بین میبرد.
با این حال، این روش محدودیتهایی نیز دارد. افزودن متادادههایی مانند $id و $ref اندازه خروجی JSON را افزایش میدهد، که ممکن است در برنامههایی با محدودیت پهنای باند مشکلساز باشد. علاوه بر این، JSON تولیدشده ممکن است برای سریالسازهای دیگر که از این متادادهها پشتیبانی نمیکنند، قابلفهم نباشد، که میتواند قابلیت همکاری (Interoperability) را کاهش دهد. توسعهدهندگان باید این عوامل را در نظر بگیرند و بسته به نیاز پروژه خود بین گزینههای Preserve و IgnoreCycles انتخاب کنند.
ارتباط با سایر مفاهیم داتنت
مدیریت ارجاعات حلقوی در System.Text.Json تنها یکی از جنبههای سریالسازی در داتنت است. این موضوع با مفاهیم دیگری مانند بهینهسازی عملکرد، مدیریت حافظه، و طراحی مدلهای داده مرتبط است. برای مثال، تولیدکنندههای منبع بخشی از استراتژی گستردهتر مایکروسافت برای کاهش سربار بازتاب و بهبود عملکرد در داتنت هستند. توسعهدهندگان میتوانند از این قابلیت در کنار سایر ویژگیهای داتنت 10، مانند AOT Compilation یا بهبودهای Span و Memory، برای ساخت برنامههایی با عملکرد بالا استفاده کنند.
علاوه بر این، درک ارجاعات حلقوی میتواند به توسعهدهندگان در طراحی بهتر مدلهای داده کمک کند. به جای ایجاد ساختارهای پیچیده با ارجاعات دوطرفه، ممکن است استفاده از مدلهای نمایشی (View Models) یا DTOها (Data Transfer Objects) در برخی سناریوها سادهتر و کارآمدتر باشد. این رویکرد نه تنها مشکلات سریالسازی را کاهش میدهد، بلکه کد را خواناتر و قابل نگهداریتر میکند.
نتیجهگیری
مدیریت ارجاعات حلقوی در تولیدکنندههای منبع JSON در داتنت 10 یک پیشرفت مهم در System.Text.Json است که به توسعهدهندگان امکان میدهد با انعطافپذیری بیشتری اشیاء پیچیده را سریالسازی کنند. با استفاده از ReferenceHandler.Preserve، میتوان ارجاعات حلقوی را به طور مؤثر مدیریت کرد، در حالی که مزایای عملکردی تولیدکنندههای منبع حفظ میشود. این قابلیت بهویژه برای برنامههایی که با مدلهای دادهای پیچیده مانند آنهایی که در Entity Framework استفاده میشوند، بسیار ارزشمند است.