عنوان:

‫مدیریت استراتژی‌های نام‌گذاری در System.Text.Json و قابلیت جدید PascalCase در NET 11.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۴/۰۷ ۰۸:۱۰
آدرس: www.dntips.ir
در دنیای توسعه نرم‌افزار مدرن، تبادل داده‌ها در قالب JSON به یک استاندارد بی‌بدیل تبدیل شده است. با این حال، یکی از چالش‌های همیشگی در زمان یکپارچه‌سازی سیستم‌های مختلف، تفاوت در قراردادهای نام‌گذاری (Naming Conventions) است. به عنوان مثال، اکوسیستم فرانت‌اند و جاوااسکریپت عموماً از camelCase استقبال می‌کند، در حالی که زبان‌هایی مانند پایتون به استراتژی snake_case تمایل دارند و در اکوسیستم دات‌نت و زبان سی‌شارپ، فرمت استاندارد و بومی اعضای کلاس‌ها به صورت PascalCase تعریف می‌شود.
کتابخانه قدرتمند System.Text.Json از زمان معرفی در نسخه‌های گذشته دات‌نت، امکان مدیریت این سیاست‌ها را فراهم کرده است. در نسخه پیش‌نمایش NET 11.، مایکروسافت با افزودن یک قابلیت رسمی و پرتقاضا تحت عنوان JsonNamingPolicy.PascalCase، پازل استراتژی‌های نام‌گذاری بومی را تکمیل کرده است. در این مقاله به بررسی عمیق این ویژگی، دلایل اهمیت آن و نحوه استفاده از آن می‌پردازیم.

مدیریت قراردادهای نام‌گذاری پیش از NET 11.
پیش از انتشار این نسخه، فریم‌ورک دات‌نت به مرور زمان استراتژی‌های متنوعی را به کلاس JsonNamingPolicy اضافه کرده بود تا نیاز توسعه‌دهندگان را بدون وابستگی به کتابخانه‌های شخص ثالث (مانند Json.NET) برطرف کند. این استراتژی‌ها شامل موارد زیر بودند:
  • CamelCase: تبدیل کلمات به صورت اولین کلمه کوچک و بقیه بزرگ (مانند firstName).
  • KebabCaseLower و KebabCaseUpper: جداسازی کلمات با خط تیره (مانند first-name یا FIRST-NAME).
  • SnakeCaseLower و SnakeCaseUpper: جداسازی کلمات با خط زیرین (مانند first_name یا FIRST_NAME).

بررسی پیاده‌سازی کد و خروجی استراتژی‌ها
برای درک بهتر نحوه عملکرد این استراتژی‌ها، ابتدا مدل داده‌ای زیر را که بر اساس قراردادهای استاندارد سی‌شارپ به صورت PascalCase نام‌گذاری شده است، در نظر بگیرید:
public sealed class Person
{
    public required string FirstName { get; set; }
    public required string Surname { get; set; }
}
در گذشته، برای سریالایز کردن این شیء به فرمت‌های مختلف از نمونه کد زیر استفاده می‌شد. اکنون در NET 11.، گزینه نهایی یعنی PascalCase نیز به این مجموعه اضافه شده‌است که به ما اجازه می‌دهد دقیقاً مطابق با قراردادهای سیستم‌های مصرف‌کننده عمل کنیم:
var person = new Person { FirstName = "James", Surname = "Bond" };

// Camel Case
Console.WriteLine(JsonSerializer.Serialize(person, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }));

// Kebab Case (Lower & Upper)
Console.WriteLine(JsonSerializer.Serialize(person, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, WriteIndented = true }));
Console.WriteLine(JsonSerializer.Serialize(person, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper, WriteIndented = true }));

// Snake Case (Lower & Upper)
Console.WriteLine(JsonSerializer.Serialize(person, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = true }));
Console.WriteLine(JsonSerializer.Serialize(person, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseUpper, WriteIndented = true }));

// Pascal Case (.NET 11 New Feature)
Console.WriteLine(JsonSerializer.Serialize(person, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.PascalCase, WriteIndented = true }));

خروجی‌های متناظر JSON
اجرای کدهای فوق، خروجی‌های ساختاریافته زیر را تولید می‌کند:
// Camel Case Output
{
  "firstName": "James",
  "surname": "Bond"
}

// Kebab Case Lower & Upper Outputs
{
  "first-name": "James",
  "surname": "Bond"
}
{
  "FIRST-NAME": "James",
  "SURNAME": "Bond"
}

// Snake Case Lower & Upper Outputs
{
  "first_name": "James",
  "surname": "Bond"
}
{
  "FIRST_NAME": "James",
  "SURNAME": "Bond"
}

// Pascal Case Output (.NET 11)
{
  "FirstName": "James",
  "Surname": "Bond"
}

چرا به سیاست صریح PascalCase نیاز داریم؟
یک سوال هوشمندانه که ممکن است برای توسعه‌دهندگان پیش بیاید این است: اگر رفتار پیش‌فرض System.Text.Json به این صورت است که نام ویژگی‌ها را دست‌نخورده نگه می‌دارد (و در نتیجه خروجی همان PascalCase کدهای سی‌شارپ می‌شود)، پس چرا مایکروسافت یک گزینه صریح به نام JsonNamingPolicy.PascalCase را اضافه کرده است؟
پاسخ این سوال در سناریوهای معماری پیشرفته و یکپارچه‌سازی وب‌سرویس‌ها نهفته است:
  • تغییر پیش‌فرض‌ها در ASP.NET Core: فریم‌ورک ASP.NET Core به صورت پیش‌فرض تنظیمات بومی سریالایزیشن خروجی‌های API را روی camelCase قرار می‌دهد. اگر در پروژه‌ای نیاز داشته باشید که کل خروجی‌های یک وب‌API یا یک کلاینت خاص به صورت PascalCase ارسال شود، در گذشته مجبور بودید مقدار PropertyNamingPolicy را برابر null قرار دهید که رویکرد چندان خوانایی نبود. وجود یک کلاس صریح، وضوح و خلوص کد (Clean Code) را افزایش می‌دهد.
  • پویایی در خط لوله‌های پردازش داده (Dynamic Pipelines): در معماری‌هایی که شیء تنظیمات (JsonSerializerOptions) یا نمونه‌ای از JsonNamingPolicy به عنوان یک شیء تزریق وابستگی (DI) یا پارامتر بین متدها و کتابخانه‌های مختلف پاس داده می‌شود، داشتن یک تمایز صریح برای فرمت پاسکال به شما اجازه می‌دهد تا بدون نیاز به شرط‌های پیچیده بر روی مقادیر null، سیاست نام‌گذاری را کنترل و بازنویسی کنید.
  • سازگاری با سیستم‌های قدیمی (Legacy Systems): بسیاری از سازمان‌ها دارای وب‌سرویس‌های قدیمی مستقر بر بستر دات‌نت فریم‌ورک‌های گذشته یا سیستم‌های بزرگ سازمانی (مثل ERPها) هستند که به شدت به بزرگی و کوچکی حروف حساس بوده و ساختار PascalCase را طلب می‌کنند. ارائه این ویژگی فرآیند مهاجرت و یکپارچه‌سازی را تسهیل می‌کند.

نتیجه‌گیری
افزوده شدن JsonNamingPolicy.PascalCase در پیش‌نمایش NET 11.، گامی مهم در جهت هماهنگی هرچه بیشتر، افزایش خوانایی کد و ارائه پلتفرمی منسجم است. با استفاده از این قابلیت، توسعه‌دهندگان دات‌نت اکنون ابزاری کامل و بدون نقص برای مدیریت هر نوع قرارداد نام‌گذاری در تعامل با دنیای بیرون از اکوسیستم خود در اختیار دارند. پیشنهاد می‌شود در زمان ارتقا به نسخه‌های جدید، از این سیاست صریح برای بهبود شفافیت در لایه‌های سریالایزیشن برنامه‌های خود استفاده کنید.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۴/۱۲ ۰۹:۳۴
    کنترل دانه-ریز (Fine-Grained) استراتژی‌های نام‌گذاری در .NET 11؛ بررسی کاربرد اتریبیوتJsonNamingPolicyAttributeدر سناریوهای پیچیده

    تا پیش از این، تنظیمات مربوط به PropertyNamingPolicy در لایه JsonSerializerOptions اعمال می‌شد؛ این یعنی سیاست انتخاب شده (مانند CamelCase یا SnakeCase) به صورت یکپارچه و سراسری (Global) بر روی تمام ویژگی‌های (Properties) یک شیء یا کل پروژه تأثیر می‌گذاشت.
    اما در دنیای واقعی و فرآیند یکپارچه‌سازی با سیستم‌های ناهمگون (Heterogeneous Systems)، گاهی با سناریوهای پیچیده و نامتعارفی روبرو می‌شویم. فرض کنید در حال اتصال به وب‌سرویسی هستید که خروجی آن به دلایل تاریخی یا خطاهای طراحی، از هیچ ساختار واحدی پیروی نمی‌کند؛ به طوری که یک ویژگی در آن به صورت snake_case و ویژگی دیگر در همان شیء به صورت camelCase تعریف شده است.
    در نسخه‌های قبلی دات‌نت، برای حل این چالش و اعمال سیاست‌های متفاوت روی ویژگی‌های یک کلاس، توسعه‌دهندگان مجبور به نوشتن یک JsonConverter سفارشی و پیچیده بودند که نیازمند مدیریت دستی کل فرآیند خواندن و نوشتن توکن‌های JSON بود.

    راهکار جدید .NET 11: اتریبیوت[JsonNamingPolicy]
    مایکروسافت در نسخه پیش‌نمایش .NET 11، قابلیت جدید و بسیار کارآمدی را معرفی کرده است که به شما اجازه می‌دهد به صورت تفکیک‌شده و نقطه‌ای، سیاست نام‌گذاری هر ویژگی را مستقیماً روی خود آن ویژگی در سطح کلاس مشخص کنید. این کار با استفاده از اتریبیوت [JsonNamingPolicy] و انام (Enum) جدید JsonKnownNamingPolicy انجام می‌شود.

    پیاده‌سازی مدل داده با سیاست‌های ترکیبی
    مدل داده زیر را در نظر بگیرید که در آن هر ویژگی با یک استراتژی نام‌گذاری متفاوت دکور شده است:
    using System.Text.Json.Serialization;
    
    public sealed class Spy 
    { 
        // اعمال سیاست حروف کوچک همراه با خط زیرین
        [JsonNamingPolicy(JsonKnownNamingPolicy.SnakeCaseLower)] 
        public required string FirstName { get; init; } 
     
        // اعمال سیاست کوهان‌شتری
        [JsonNamingPolicy(JsonKnownNamingPolicy.CamelCase)] 
        public required string Surname { get; init; } 
     
        // اعمال سیاست کوهان‌شتری
        [JsonNamingPolicy(JsonKnownNamingPolicy.CamelCase)] 
        public required DateOnly DateOfBirth { get; init; } 
    }

    فرآیند سریالایزیشن و خروجی ترکیبی
    نکته جذاب اینجاست که در زمان سریالایز کردن این شیء، دیگر نیازی به تعریف سیاست‌های سراسری در JsonSerializerOptions نیست. موتور System.Text.Json به طور خودکار اتریبیوت‌های ثبت‌شده روی ویژگی‌ها را کشف کرده و اعمال می‌کند:
    using System.Text.Json;
    
    var spy = new Spy 
    { 
        FirstName = "James", 
        Surname = "Bond", 
        DateOfBirth = new DateOnly(1950, 1, 1) 
    }; 
     
    var options = new JsonSerializerOptions 
    { 
        WriteIndented = true // فقط برای فرمت‌دهی خوانای خروجی
    }; 
     
    Console.WriteLine(JsonSerializer.Serialize(spy, options));
    خروجی متناظر JSON:
    { 
      "first_name": "James", 
      "surname": "Bond", 
      "dateOfBirth": "1950-01-01" 
    }
    همان‌طور که در خروجی مشاهده می‌کنید، ویژگی FirstName به صورت تک‌عضوی از سیاست snake_case پیروی کرده، در حالی که سایر ویژگی‌ها فرمت camelCase را به خود گرفته‌اند.
    تحلیل فنی و نکات تکمیلی (غنی‌سازی بحث)
    توسعه‌دهندگان در زمان استفاده از این قابلیت جدید باید به دو نکته کلیدی و تقدم اجرا (Precedence) توجه داشته باشند:
    • اولویت بالاتر نسبت به تنظیمات سراسری (Global Options Overriding): اگر شما در شیء JsonSerializerOptions یک سیاست سراسری مثل PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower تعریف کرده باشید، اما روی یک ویژگی خاص اتریبیوت [JsonNamingPolicy] را قرار دهید، تنظیماتِ نقطه‌ایِ اتریبیوت اولویت بالاتر داشته و تنظیمات سراسری را برای آن ویژگی خاص نادیده می‌گیرد (Override می‌کند).
    • تفاوت با [JsonPropertyName]: شاید بپرسید این اتریبیوت چه تفاوتی با [JsonPropertyName("custom_name")] دارد؟ اتریبیوت قدیمیِ JsonPropertyName نام ویژگی را به صورت یک رشته ثابت (Hardcoded String) تغییر می‌دهد. اما [JsonNamingPolicy] یک قانون یا استراتژی را به ویژگی تزریق می‌کند. این رویکرد به ویژه زمانی مفید است که نام ویژگی‌ها در کدهای شما به دلایلی تغییر کند؛ در این حالت، سیاست نام‌گذاری همچنان به درستی روی نام جدید اعمال می‌شود و نیازی به تغییر رشته‌های هاردکد شده نیست.

    نتیجه‌گیری
    معرفی اتریبیوت [JsonNamingPolicy] در .NET 11، کنترل دانه-ریز (Fine-grained) و بسیار منعطفی را بدون نیاز به نوشتن بویلرپلیت‌کدها (Boilerplate code) یا کانورترهای سفارشی در اختیار توسعه‌دهنده قرار می‌دهد. این قابلیت، توسعه نرم‌افزارهای دات‌نتی را در مواجهه با سیستم‌های میراثی (Legacy) یا APIهای شخص ثالث با طراحی غیراستاندارد، بیش از پیش لذت‌بخش و ساده‌تر می‌کند.