مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 5 - آشنایی با کلاس ServiceDescriptor
در بخش پنجم از سری نوشتار «تزریق وابستگی‌ها در ASP.NET Core»، می‌خواهیم به شرح کلاس ServiceDescriptor بپردازیم. اگر تعریف اینترفیس IServiceCollection را مشاهده کنیم، می‌بینیم که IServicecollection در واقع لیستی از اشیائی از نوع ServiceDescriptor را نگهداری می‌کند:
namespace Microsoft.Extensions.DependencyInjection
{
    public interface IServiceCollection : 
       ICollection<ServiceDescriptor>, IEnumerable<ServiceDescriptor>,
       IEnumerable, IList<ServiceDescriptor>
    {
    }
}

ServiceProvider و مؤلفه‌های درونی آن، از یک مجموعه از ServiceDescriptor‌ها برای برنامه‌ی شما بر اساس سرویس‌های ثبت شده‌ی توسط IServiceCollection استفاده می‌کنند. ServiceDescriptor حاوی اطلاعاتی در مورد سرویس‌های ثبت شده‌است. اگر به کد منبع این کلاس برویم، می‌بینیم پنج Property اصلی دارد که با استفاده از آن‌ها اطلاعات یک سرویس ثبت و نگهداری می‌شوند. با استفاده از این  اطلاعات در هنگام اجرا ، DI Container به واکشی و ساخت نمونه‌هایی از سرویس درخواستی اقدام می‌کند:

public Type ImplementationType { get; }
public object ImplementationInstance { get; }
public Func<IServiceProvider, object> ImplementationFactory { get; }
public ServiceLifetime Lifetime { get; }
public Type ServiceType { get; }

هر کدام از این Property ‌ها کاربرد خاص خود را دارند:

  • · ServiceType : نوع سرویسی را که می‌خواهیم ثبت شود، مشخص می‌کنیم ( مثلا اینترفیس IMessageService ) .  
  • · ImplementionType : نوع پیاده سازی سرویس مورد نظرمان را مشخص می‌کند ( مثلا کلاس MessageService ).  
  • · LifeTime : طول حیات سرویس را مشخص می‌کند. DI Container بر اساس این ویژگی، اقدام به ساخت و از بین بردن نمونه‌هایی از سرویس می‌کند.  
  • · ImplementionInstance : نمونه‌ی ساخته شده‌ی از سرویس است.  
  • · ImplementionFactory : یک Delegate است که چگونگی ساخته شدن یک نمونه از پیاده سازی سرویس را در خود نگه می‌دارد. این Delegate یک IServiceProvider را به عنوان ورودی دریافت می‌کند و یک object را بازگشت می‌دهد.

به صورت عادی، در سناریوهای معمول ثبت سرویس‌ها درون IServiceCollection، نیازی به استفاده از ServiceDescriptor نیست؛ ولی اگر بخواهیم سرویس‌ها را به روش‌های پیشرفته‌تری ثبت کنیم، مجبوریم که به صورت مستقیم با این کلاس کار کنیم.

 

می توانیم یک ServiceDesciriptor را به روش‌های زیر تعریف کنیم:

var serviceDescriptor1 = new ServiceDescriptor(
   typeof(IMessageServiceB),
   typeof(MessageServiceBB),
   ServiceLifetime.Scoped);

var serviceDescriptor2 = ServiceDescriptor.Describe(
   typeof(IMessageServiceB),
   typeof(MessageServiceBB),
   ServiceLifetime.Scoped);

var serviceDescriptor3 = ServiceDescriptor.Singleton(typeof(IMessageServiceB), typeof(MessageServiceBB));

var serviceDescriptor4 = ServiceDescriptor.Singleton<IMessageServiceB, MessageServiceBB>();
در بالا روش‌های تعریف یک ServiceDescriptor را می‌بینید. اولین متد و تعریف پارامترها در سازنده‌ها، روش پایه است؛ ولی برای راحتی کار، توسعه دهندگان تعدادی متد static نیز تعریف کرده‌اند که خروجی آنها یک نمونه از ServiceDescriptor است.

همانطور که دیدیم، IServiceCollection در واقع لیست و مجموعه‌ای از اشیاء است که از نمونه‌های جنریک IServiceCollection ، IList ، IEnumerable و Ienumberabl ارث بری می‌کند؛ بنابراین می‌توان از متدهای تعریف شده‌ی در این اینترفیس‌ها برای IServiceCollection نیز استفاده کرد. حالا ما برای اضافه کردن این سرویس‌های جدید، بدین طریق عمل می‌کنیم:

Services.Add(serviceDescriptor1);

استفاده از متدهای TryAdd() 

به کد زیر نگاه کنید :

services.AddScoped<IMessageServiceB, MessageServiceBA>();
services.AddScoped<IMessageServiceB, MessageServiceBB>();
همانطور که می‌بینید، در اینجا یک اینترفیس را دوبار ثبت کردیم. در این حالت موقع واکشی سرویس، DI Container آخرین نمونه‌ی ثبت شده‌ی برای اینترفیس را واکشی کرده و نمونه سازی می‌کند و به کلاس‌ها تزریق می‌کند. این یکی از مواردی است که ترتیب ثبت کردن سرویس‌های مهم است.

برای جلوگیری از این خطا می‌توانیم از متدهای TryAddSingleton() ، TryAddScoped() و TryAddTransient() استفاده کنیم. این متدها درون فضای نام Microsoft.Extionsion.DependencyInjection.Extension قرار دارند.

عملکرد کلی این متدها درست مثل متد‌های Add() است؛ با این تفاوت که این متد ابتدا IServiceCollection را جستجو می‌کند و اگر برای type مورد نظر سرویسی ثبت نشده بود، آن را ثبت می‌کند:

services.TryAddScoped<IMessageServiceB, MessageServiceBA>();
services.TryAddScoped<IMessageServiceB, MessageServiceBB>();

جایگذاری یک سرویس با نمونه‌ای دیگر

گاهی اوقات می‌خواهیم یک پیاده سازی دیگر را بجای پیاده سازی فعلی، در DI Container ثبت کنیم. در این حالت از متد Replace() بر روی IServiceCollection برای این کار استفاده می‌کنیم. این متد فقط یک ServiceDescriptor را به عنوان پارامتر ورودی می‌گیرد:

services.Replace(serviceDescriptor3);
ناگفته نماند که متد Replace() فقط اولین سرویس را با نمونه‌ی مورد نظر ما جایگذاری می‌کند. اگر می‌خواهید تمام نمونه سرویس‌های ثبت شده را برای یک نوع حذف کنید، می‌توانید از متد RemoveAll() استفاده کنید:
services.RemoveAll<IMessageServiceB>();

معمولا در پروژه‌های معمول خودمان نیازی به استفاده از Replace() و RemoveAll() نداریم؛ مگر اینکه بخواهیم پیاده سازی اختصاصی خودمان را برای سرویس‌های درونی فریم ورک یا کتابخانه‌های شخص ثالث، بجای پیاده سازی پیش فرض، ثبت و استفاده کنیم.  

 

AddEnumerable()

فرض کنید دارید برنامه‌ی نوبت دهی یک کلینیک را می‌نویسید و به صورت پیش فرض از شما خواسته‌اند که هنگام صدور نوبت، این قوانین را بررسی کنید:

  •   هر شخص در هفته نتواند بیش از 2 نوبت برای یک تخصص بگیرد.
  •   اگر شخص در ماه بیش از 3 نوبت رزرو شده داشته باشد ولی مراجعه نکرده باشد، تا پایان ماه، امکان رزرو نوبت را نداشته باشد .
  •   تعداد نوبت‌های ثبت شده‌ی برای پزشک در آن روز نباید بیش از تعدادی باشد که پزشک پذیرش می‌کند.
  •   و ...

یک روش معمول برای پیاده سازی این قابلیت، ساخت سرویسی برای ثبت نوبت است که درون آن متدی برای بررسی کردن قوانین ثبت نام وجود دارد. خب، ما این کار را انجام می‌دهیم. تست‌های واحد و تست‌های جامع را هم می‌نویسیم و بعد برنامه را انتشار می‌دهیم و همه چیز خوب است؛ تا اینکه مالک محصول یک نیازمندی جدید را می‌خواهد که در آن ما باید قانون زیر را در هنگام ثبت نوبت بررسی کنیم:

  •   نوبت‌های ثبت شده برای یک شخص نباید دارای تداخل باشند.

در این حالت ما باید دوباره سرویس Register را باز کنیم و به متد بررسی کردن قوانین برویم و دوباره کدهایی را برای بررسی کردن قانون جدید بنویسیم و احتمالا کد ما به این صورت خواهد شد:

public class RegisterAppointmentService : RegisterAppointmentService
{
  public Task<Result> RegisterAsync(
    PatientInfoDTO patientIfno , DateTimeOffset requestedDateTime ,
    PhysicianId phusicianId )
  {
      CheckRegisterantionRule(patientInfo);
      // code here
  }

  private Task CheckRegisterationRule(PatientInfoDTO patientInfo)
  {
       CheckRule1(patientInfo);
       CheckRule2(patientInfo);
       CheckRule3(patientInfo);
  }
}  

در این حالت باید به ازای هر قانون جدید، به متد CheckRegisterationRule برویم و به ازای هر قانون، یک متد private جدید را بسازیم. مشکل این روش این است که در این حالت ما مجبوریم با هر کم و زیاد شدن قانون، این کلاس را باز کنیم و آن را تغییر دهیم و با هر تغییر دوباره، تست‌های واحد آن را دوباره نویسی کنیم. در یک کلام در کد بالا اصول Separation of Concern و  Open/Closed Principle را رعایت نمی‌شود.

یک راهکار این است که یک سرویس جداگانه را برای بررسی کردن قوانین بنویسیم و آن را به سرویس ثبت نوبت تزریق کنیم:

public class ICheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService
{
     public Task CheckRegisterantionRule(PatientInfoDTO patientInfo)
     {
                CheckRule1(patientInfo);
                CheckRule2(patientInfo);
                CheckRule3(patientInfo);
      }
}

public class RegisterAppointmentService : IRegisterAppointmentService
{
  private ICheckRegisterationRuleForAppointmentService  _ruleChecker;
 
  public RegisterAppointmentService (RegisterAppointmentService  ruleChecker)
  {
          _ruleChecker = ruleChecker;  
  }

  public Task<Result> RegisterAsync(
     PatientInfoDTO patientIfno , 
     DateTimeOffset requestedDateTime , 
     PhysicianId phusicianId )
  {
             _ruleChecker.CheckRegisterantionRule(patientInfo);
                // code here
  }
}

با این کار وظیفه‌ی چک کردن قوانین و وظیفه‌ی ثبت و ذخیره سازی قوانین را از یکدیگر جدا کردیم؛ ولی همچنان در سرویس بررسی کردن قوانین، اصل Open/Closed رعایت نشده‌است. خب راه حل چیست !؟

یکی از راه حل‌های موجود، استفاده از الگوی قوانین یا Rule Pattern است. برای اجرای این الگو، می‌توانیم با تعریف یک اینترفیس کلی برای بررسی کردن قانون، به ازای هر قانون یک پیاده سازی اختصاصی را داشته باشیم:


interface IAppointmentRegisterationRule
{
  Task CheckRule(PatientInfo patientIfno);
}

public class AppointmentRegisterationRule1 : IAppointmentRegisterationRule
{
      public Task CheckRule(PatientInfo patientIfno)
      {
          Console.WriteLine("Rule 1 is checked");
          return Task.CompletedTask;
      }
}

public class AppointmentRegisterationRule2 : IAppointmentRegisterationRule
{
     public Task CheckRule(PatientInfo patientIfno)
     {
Console.WriteLine("Rule 2 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule3 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 3 is checked"); return Task.CompletedTask; } } public class AppointmentRegisterationRule4 : IAppointmentRegisterationRule { public Task CheckRule(PatientInfo patientIfno) { Console.WriteLine("Rule 4 is checked"); return Task.CompletedTask; } }
حالا که ما قوانین خودمان را تعریف کردیم، به روش زیر می‌توانیم آن‌ها را درون سازنده ثبت کنیم:
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule1>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule2>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule3>();
services.AddScoped<IAppointmentRegisterationRule, AppointmentRegisterationRule4>();
حالا می‌توانیم درون سازنده‌ی سرویس مورد نظرمان، لیستی از سرویس‌های ثبت شده‌ی برای یک نوع خاص را به با استفاده از اینترفیس جنریک IEnumerable<T> دریافت کنیم که در اینجا T، برابر نوع سرویس مورد نظرمان است:
public class CheckRegisterationRuleForAppointmentService : ICheckRegisterationRuleForAppointmentService
{
       private IEnumerable<IAppointmentRegisterationRule> _rules ;

       public CheckRegisterationRuleForAppointmentService(IEnumerable<IAppointmentRegisterationRule> rules)
       {
           _rules = rules;
       }

      public Task CheckRegisterantionRule(PatientInfoDTO patientInfo)
      {
          foreach(var rule in rules)
          {
                rule.CheckRule(patientInfo);
          }
      }
}
با این تغییرات، هر زمانیکه خواستیم می‌توانیم با استفاده از DI Container، قوانین جدیدی را اضافه یا کم کنیم و با این کار، اصل Open/Closed را نیز رعایت کرده‌ایم.

 کد بالا به نظر کامل می‌آید ولی مشکلی دارد! اگر در DI Container برای IAppointmentRegisterationRule یک قانون را دو یا چند بار ثبت کنیم، در هر بار بررسی کردن قوانین، آن را به همان تعداد بررسی می‌کند و اگر این فرآیند منابع زیادی را به کار می‌گیرد، می‌تواند عملکرد برنامه‌ی ما را به هم بریزد.  برای جلوگیری از این مشکل، از متد TryAddEnumerabl() استفاده می‌کنیم که لیستی از ServiceDescriptor ‌ها را می‌گیرد و هر serviceDescriptor را فقط یکبار ثبت می‌کند:

services.TryAddEnumerable(new[] {
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule1)),
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule2)),
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule3)),
  ServiceDescriptor.Scoped(typeof(IAppointmentRegisterationRule), typeof(AppointmentRegisterationRule4)),
});

نظرات مطالب
jQuery Ajax و نحوه صحیح ارسال مقادیر به یک وب سرویس
بسیار عالی بود . یه سوالی داشتم و اینکه برای کار با jquery ajax باید در visualstudio پروژه Enabale Ajax ایجاد نمود یا اینکه با پروژه معمولی هم کار میکنه چرا که هنگام پاس دادن اطلاعات بصورت json فایر باگ میگفت فرمتت صحیح نیست و وقتی که با پروژه Enable ajax کار کردم مشکل حل شد در این مورد یه توضیحی میدید . با سپاس فراوان/
نظرات مطالب
آشنایی با WebDav و نحوه استفاده از آن
سلام. با تشکر از آموزش و تهیه کتابخونه.

وقتی فایل رو با word باز میکنم، بصورت readonly باز میشه و گزینه save بصورت آیکن sync نیست.
هم توی نمونه سورس کد گیت و هم یک وب‌اپ که خودم ایجاد کردم و رفرنس و پکیج نصب کردم مشکل بود. روی iis هم پابلیش گذاشتم فرقی نکرد.
نظرات مطالب
Globalization در ASP.NET MVC - قسمت دوم
کلاس BuildManager مگه اجرایی هست؟
موضوع اینه که مشکل من با Build پروژه نیست (Build با موفقیت انجام میشود)
موضوع عدم ایجاد پوشه fa-ir و بالطبع فایل dll داخل آن می‌باشد.
خواهشمند است اگر سوال بنده جاییش نامفهوم است بفرمائید که اصلاح نمایم تا به پاسخ برسم.
نظرات مطالب
اجرای وظایف زمان بندی شده با Quartz.NET - قسمت دوم
چون Threadیی که ایجاد میشه، اصطلاحاً Foreground هست و باید به Background تبدیل بشه.
جزئیات بیشتر...
در ضمن توجه داشته باشید که اگر در وسط اجرای یک فرایند اون رو به زور متوقف کنید، مشکل هرز رفتن منابع رو خواهید داشت. در نظر بگیرید که فایلی رو باز کردید و قصد نوشتن در اون رو دارید، در همین هنگام پروسه رو قطع می‌کنید. فایل باز می‌مونه و پروسس‌های دیگه نمی‌تونن به اون دسترسی داشته باشن. به نظر من بهتره رفتار پیش فرض Quartz.NET رو تغییر ندید و بر روی منطق برنامه‌ی خودتون معطوف باشید.
بازخوردهای پروژه‌ها
سوال در مورد automapper
سلام
ممنون از اینکه پروژه تون رو دراختیار ما گذاشتید. من خیلی از پروژه شما ایده‌های خوبی گرفتم.
یه سوالی که داشتم در مورد automapper بود. طبق مقالات مرتبط با automapper که خوندم باید config‌های mapper در startup پروژه قرار داده بشه ولی من چنین بخشی تو پروژه پیدا نکردم ، در عوض یک سری فایل از نوع Profile پیدا کردم که متد Configure  رو override کرده بود. میخواستم بپرسم این Profile‌ها چی هستن کارشون چیه و آیا این متد Configure شون بصورت اتوماتیک اجرا میشه؟ 
یه سوال دیگه ای که داشتم در مورد متد IgnoreAllNonExisting در پروژه AutoMapperProfile بود، این متد چه کاری انجام میده؟
متشکرم
نظرات مطالب
نحوه اجباری کردن استفاده از WWW در ASP.NET MVC
سلام و با تشکر. می‌خواستم بدونم چجوری میتوان پسوند آدرس Url رو حذف کنم، نمی‌دونم تا چه اندازه برای کیفیت سایت مفید هست ولی بیشتر سایت‌های که من می‌بینم این کار رو انجام می‌دند. من با asp.net form  کار می‌کنم.
مطالب
Globalization در ASP.NET MVC
اگر بازار هدف یک محصول شامل چندین کشور، منطقه یا زبان مختلف باشد، طراحی و پیاده سازی آن برای پشتیبانی از ویژگی‌های چندزبانه یک فاکتور مهم به حساب می‌آید. یکی از بهترین روشهای پیاده سازی این ویژگی در دات نت استفاده از فایلهای Resource است. درواقع هدف اصلی استفاده از فایلهای Resource نیز Globalization است. Globalization برابر است با Internationalization + Localization که به اختصار به آن g11n میگویند. در تعریف، Internationalization (یا به اختصار i18n) به فرایند طراحی یک محصول برای پشتیبانی از فرهنگ(culture)‌ها و زبانهای مختلف و Localization (یا L10n) یا بومی‌سازی به شخصی‌سازی یک برنامه برای یک فرهنگ یا زبان خاص گفته میشود. (اطلاعات بیشتر در اینجا).
استفاده از این فایلها محدود به پیاده سازی ویژگی چندزبانه نیست. شما میتوانید از این فایلها برای نگهداری تمام رشته‌های موردنیاز خود استفاده کنید. نکته دیگری که باید بدان اشاره کرد این است که تقرببا تمامی منابع مورد استفاده در یک محصول را میتوان درون این فایلها ذخیره کرد. این منابع در حالت کلی شامل موارد زیر است:
- انواع رشته‌های مورد استفاده در برنامه چون لیبل‌ها و پیغام‌ها و یا مسیرها (مثلا نشانی تصاویر یا نام کنترلرها و اکشنها) و یا حتی برخی تنظیمات ویژه برنامه (که نمیخواهیم براحتی قابل نمایش یا تغییر باشد و یا اینکه بخواهیم با تغییر زبان تغییر کنند مثل direction و امثال آن)
- تصاویر و آیکونها و یا فایلهای صوتی و انواع دیگر فایل ها
- و ...
 نحوه بهره برداری از فایلهای Resource در دات نت، پیاده سازی نسبتا آسانی را در اختیار برنامه نویس قرار میدهد. برای استفاده از این فایلها نیز روشهای متنوعی وجود دارد که در مطلب جاری به چگونگی استفاده از آنها در پروژه‌های ASP.NET MVC پرداخته میشود.

Globalization در دات نت
فرمت نام یک culture دات نت (که در کلاس CultureInfo پیاده شده است) بر اساس استاندارد RFC 4646 (^ و ^) است. (در اینجا اطلاعاتی راجع به RFC یا Request for Comments آورده شده است). در این استاندارد نام یک فرهنگ (کالچر) ترکیبی از نام زبان به همراه نام کشور یا منطقه مربوطه است. نام زبان برپایه استاندارد ISO 639 که یک عبارت دوحرفی با حروف کوچک برای معرفی زبان است مثل fa برای فارسی و en برای انگلیسی و نام کشور یا منطقه نیز برپایه استاندارد ISO 3166 که یه عبارت دوحرفی با حروف بزرگ برای معرفی یک کشور یا یک منطقه است مثل IR برای ایران یا US برای آمریکاست. برای نمونه میتوان به fa-IR برای زبان فارسی کشور ایران و یا en-US برای زبان انگلیسی آمریکایی اشاره کرد. البته در این روش نامگذاری یکی دو مورد استثنا هم وجود دارد (اطلاعات کامل کلیه زبانها: National Language Support (NLS) API Reference). یک فرهنگ خنثی (Neutral Culture) نیز تنها با استفاده از دو حرف نام زبان و بدون نام کشور یا منطقه معرفی میشود. مثل fa برای فارسی یا de برای آلمانی. در این بخش نیز دو استثنا وجود دارد (^).
در دات نت دو نوع culture وجود دارد: Culture و UICulture. هر دوی این مقادیر در هر Thread مقداری منحصربه فرد دارند. مقدار Culture بر روی توابع وابسته به فرهنگ (مثل فرمت رشته‌های تاریخ و اعداد و پول) تاثیر میگذارد. اما مقدار UICulture تعیین میکند که سیستم مدیریت منابع دات نت (Resource Manager) از کدام فایل Resource برای بارگذاری داده‌ها استفاده کند. درواقع در دات نت با استفاده از پراپرتی‌های موجود در کلاس استاتیک Thread برای ثرد جاری (که عبارتند از CurrentCulture و CurrentUICulture) برای فرمت کردن و یا انتخاب Resource مناسب تصمیم گیری میشود. برای تعیین کالچر جاری به صورت دستی میتوان بصورت زیر عمل کرد:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR");
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("fa-IR");
دراینجا باید اشاره کنم که کار انتخاب Resource مناسب با توجه به کالچر ثرد جاری توسط ResourceProviderFactory پیشفرض دات نت انجام میشود. در مطالب بعدی به نحوه تعریف یک پرووایدر شخصی سازی شده هم خواهم پرداخت.

پشتیبانی از زبانهای مختلف در MVC
برای استفاده از ویژگی چندزبانه در MVC دو روش کلی وجود دارد.
1. استفاده از فایلهای Resource برای تمامی رشته‌های موجود
2. استفاده از View‌های مختلف برای هر زبان
البته روش سومی هم که از ترکیب این دو روش استفاده میکند نیز وجود دارد. انتخاب روش مناسب کمی به سلیقه‌ها و عادات برنامه نویسی بستگی دارد. اگر فکر میکنید که استفاده از ویوهای مختلف به دلیل جداسازی مفاهیم درگیر در کالچرها (مثل جانمایی اجزای مختلف ویوها یا بحث Direction) باعث مدیریت بهتر و کاهش هزینه‌های پشتیبانی میشود بهتر است از روش دوم یا ترکیبی از این دو روش استفاده کنید. خودم به شخصه سعی میکنم از روش اول استفاده کنم. چون معتقدم استفاده از ویوهای مختلف باعث افزایش بیش از اندازه حجم کار میشود. اما در برخی موارد استفاده از روش دوم یا ترکیبی از دو روش میتواند بهتر باشد.

تولید فایلهای Resource
بهترین مکان برای نگهداری فایلهای Resource در یک پروژه جداگانه است. در پروژه‌های از نوع وب‌سایت پوشه‌هایی با نام App_GlobalResources یا App_LocalResources وجود دارد که میتوان از آنها برای نگهداری و مدیریت این نوع فایلها استفاده کرد. اما همانطور که در اینجا توضیح داده شده است این روش مناسب نیست. بنابراین ابتدا یک پروژه مخصوص نگهداری فایلهای Resource ایجاد کنید و سپس اقدام به تهیه این فایلها نمایید. سعی کنید که عنوان این پروژه به صورت زیر باشد. برای کسب اطلاعات بیشتر درباره نحوه نامگذاری اشیای مختلف در دات نت به این مطلب رجوع کنید.
<SolutionName>.Resources
برای افزودن فایلهای Resource به این پروژه ابتدا برای انتخاب زبان پیش فرض محصول خود تصمیم بگیرید. پیشنهاد میکنم که از زبان انگلیسی (en-US) برای اینکار استفاده کنید. ابتدا یک فایل Resource (با پسوند resx.) مثلا با نام Texts.resx به این پروژه اضافه کنید. با افزودن این فایل به پروژه، ویژوال استودیو به صورت خودکار یک فایل cs. حاوی کلاس متناظر با این فایل را به پروژه اضافه میکند. این کار توسط ابزار توکاری به نام ResXFileCodeGenerator انجام میشود. اگر به پراپرتی‌های این فایل resx. رجوع کنید میتوانید این عنوان را در پراپرتی Custom Tool ببینید. البته ابزار دیگری برای تولید این کلاسها نیز وجود دارد. این ابزارهای توکار برای سطوح دسترسی مخنلف استفاده میشوند. ابزار پیش فرض در ویژوال استودیو یعنی همان ResXFileCodeGenerator، این کلاسها را با دسترسی internal تولید میکند که مناسب کار ما نیست. ابزار دیگری که برای اینکار درون ویژوال استودیو وجود دارد PublicResXFileCodeGenerator است و همانطور که از نامش پیداست از سطح دسترسی public استفاده میکند. برای تغییر این ابزار کافی است تا عنوان آن را دقیقا در پراپرتی Custom Tool تایپ کنید.

نکته: درباره پراپرتی مهم Build Action این فایلها در مطالب بعدی بیشتر بحث میشود.
برای تعیین سطح دسترسی Resource موردنظر به روشی دیگر، میتوانید فایل Resource را باز کرده و Access Modifier آن را به Public تغییر دهید.

سپس برای پشتیبانی از زبانی دیگر، یک فایل دیگر Resource به پروژه اضافه کنید. نام این فایل باید همنام فایل اصلی به همراه نام کالچر موردنظر باشد. مثلا برای زبان فارسی عنوان فایل باید Texts.fa-IR.resx یا به صورت ساده‌تر برای کالچر خنثی (بدون نام کشور) Texts.fa.resx باشد. دقت کنید اگر نام فایل را در همان پنجره افزودن فایل وارد کنید ویژوال استودیو این همنامی را به صورت هوشمند تشخیص داده و تغییراتی را در پراپرتی‌های پیش فرض فایل Resource ایجاد میکند.
نکته: این هوشمندی مرتبه نسبتا بالایی دارد. بدین صورت که تنها درصورتیکه عبارت بعد از نام فایل اصلی Resource (رشته بعد از نقطه مثلا fa در اینجا) متعلق به یک کالچر معتبر باشد این تغییرات اعمال خواهد شد.
مهمترین این تغییرات این است که ابزاری را برای پراپرتی Custom Tool این فایلها انتخاب نمیکند! اگر به پراپرتی فایل Texts.fa.resx مراجعه کنید این مورد کاملا مشخص است. در نتیجه دیگر فایل cs. حاوی کلاسی جداگانه برای این فایل ساخته نمیشود. همچنین اگر فایل Resource جدید را باز کنید میبنید که برای Access Modifier آن گزینه No Code Generation انتخاب شده است.
در ادامه شروع به افزودن عناوین موردنظر در این دو فایل کنید. در اولی (بدون نام زبان) رشته‌های مربوط به زبان انگلیسی و در دومی رشته‌های مربوط به زبان فارسی را وارد کنید. سپس در هرجایی که یک لیبل یا یک رشته برای نمایش وجود دارد از این کلیدهای Resource استفاده کنید مثل:
<SolutionName>.Resources.Texts.Save
<SolutionName>.Resources.Texts.Cancel

استفاده از Resource در ویومدل ها
دو خاصیت معروفی که در ویومدلها استفاده میشوند عبارتند از: DisplayName و Required. پشتیبانی از کلیدهای Resource به صورت توکار در خاصیت Required وجود دارد. برای استفاده از آنها باید به صورت زیر عمل کرد:
[Required(ErrorMessageResourceName = "ResourceKeyName", ErrorMessageResourceType = typeof(<SolutionName>.Resources.<ResourceClassName>))]
در کد بالا باید از نام فایل Resource اصلی (فایل اول که بدون نام کالچر بوده و به عنوان منبع پیشفرض به همراه یک فایل cs. حاوی کلاس مربوطه نیز هست) برای معرفی ErrorMessageResourceType استفاده کرد. چون ابزار توکار ویژوال استودیو از نام این فایل برای تولید کلاس مربوطه استفاده میکند.
متاسفانه خاصیت DisplayName که در فضای نام System.ComponentModel (در فایل System.dll) قرار دارد قابلیت استفاده از کلیدهای Resource را به صورت توکار ندارد. در دات نت 4 خاصیت دیگری در فضای نام System.ComponentModel.DataAnnotations به نام Display (در فایل System.ComponentModel.DataAnnotations.dll) وجود دارد که این امکان را به صورت توکار دارد. اما قابلیت استفاده از این خاصیت تنها در MVC 3 وجود دارد. برای نسخه‌های قدیمیتر MVC امکان استفاده از این خاصیت حتی اگر نسخه فریمورک هدف 4 باشد وجود ندارد، چون هسته این نسخه‌های قدیمی امکان استفاده از ویژگی‌های جدید فریمورک با نسخه بالاتر را ندارد. برای رفع این مشکل میتوان کلاس خاصیت DisplayName را برای استفاده از خاصیت Display به صورت زیر توسعه داد:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
  {
    private readonly DisplayAttribute _display;
    public LocalizationDisplayNameAttribute(string resourceName, Type resourceType)
    {
      _display = new DisplayAttribute { ResourceType = resourceType, Name = resourceName };
    }
    public override string DisplayName
    {
      get
      {
        try
        {
          return _display.GetName();
        }
        catch (Exception)
        {
          return _display.Name;
        }
      }
    }
  }
در این کلاس با ترکیب دو خاصیت نامبرده امکان استفاده از کلیدهای Resource فراهم شده است. در پیاده سازی این کلاس فرض شده است که نسخه فریمورک هدف حداقل برابر 4 است. اگر از نسخه‌های پایین‌تر استفاده میکنید در پیاده سازی این کلاس باید کاملا به صورت دستی کلید موردنظر را از Resource معرفی شده بدست آورید. مثلا به صورت زیر:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
{
    private readonly PropertyInfo nameProperty;
    public LocalizationDisplayNameAttribute(string displayNameKey, Type resourceType = null)
        : base(displayNameKey)
    {
        if (resourceType != null)
            nameProperty = resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);
    }
    public override string DisplayName
    {
        get
        {
            if (nameProperty == null) base.DisplayName;
            return (string)nameProperty.GetValue(nameProperty.DeclaringType, null);
        }
    }
}
برای استفاده از این خاصیت جدید میتوان به صورت زیر عمل کرد:
[LocalizationDisplayName("ResourceKeyName", typeof(<SolutionName>.Resources.<ResourceClassName>))]
البته بیشتر خواص متداول در ویومدلها از ویژگی موردبحث پشتیبانی میکنند.
نکته: به کار گیری این روش ممکن است در پروژه‌های بزرگ کمی گیج کننده و دردسرساز بوده و باعث پیچیدگی بی‌مورد کد و نیز افزایش بیش از حد حجم کدنویسی شود. در مقاله آقای فیل هک (Model Metadata and Validation Localization using Conventions) روش بهتر و تمیزتری برای مدیریت پیامهای این خاصیت‌ها آورده شده است.

پشتیبانی از ویژگی چند زبانه
مرحله بعدی برای چندزبانه کردن پروژه‌های MVC تغییراتی است که برای مدیریت Culture جاری برنامه باید پیاده شوند. برای اینکار باید خاصیت CurrentUICulture در ثرد جاری کنترل و مدیریت شود. یکی از مکانهایی که برای نگهداری زبان جاری استفاده میشود کوکی است. معمولا برای اینکار از کوکی‌های دارای تاریخ انقضای طولانی استفاده میشود. میتوان از تنظیمات موجود در فایل کانفیگ برای ذخیره زبان پیش فرض سیستم نیز استفاه کرد.
روشی که معمولا برای مدیریت زبان جاری میتوان از آن استفاده کرد پیاده سازی یک کلاس پایه برای تمام کنترلرها است. کد زیر راه حل نهایی را نشان میدهد:
public class BaseController : Controller
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    protected override void ExecuteCore()
    {
      var cookie = HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.ExecuteCore();
    }
  }
راه حل دیگر استفاده از یک ActionFilter است که نحوه پیاده سازی یک نمونه از آن در زیر آورده شده است:
public class LocalizationActionFilterAttribute : ActionFilterAttribute
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      var cookie = filterContext.HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        filterContext.HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.OnActionExecuting(filterContext);
    }
  }
نکته مهم: تعیین زبان جاری (یعنی همان مقداردهی پراپرتی CurrentCulture ثرد جاری) در یک اکشن فیلتر بدرستی عمل نمیکند. برای بررسی بیشتر این مسئله ابتدا به تصویر زیر که ترتیب رخ‌دادن رویدادهای مهم در ASP.NET MVC را نشان میدهد دقت کنید:

همانطور که در تصویر فوق مشاهده میکنید رویداد OnActionExecuting که در یک اکشن فیلتر به کار میرود بعد از عملیات مدل بایندینگ رخ میدهد. بنابراین قبل از تعیین کالچر جاری، عملیات validation و یافتن متن خطاها از فایلهای Resource انجام میشود که منجر به انتخاب کلیدهای مربوط به کالچر پیشفرض سرور (و نه آنچه که کاربر تنظیم کرده) خواهد شد. بنابراین استفاده از یک اکشن فیلتر برای تعیین کالچر جاری مناسب نیست. راه حل مناسب استفاده از همان کنترلر پایه است، زیرا متد ExecuteCore قبل از تمامی این عملیات صدا زده میشود. بنابرابن همیشه کالچر تنظیم شده توسط کاربر به عنوان مقدار جاری آن در ثرد ثبت میشود.

امکان تعیین/تغییر زبان توسط کاربر
برای تعیین یا تغییر زبان جاری سیستم نیز روشهای گوناگونی وجود دارد. استفاده از زبان تنظیم شده در مرورگر کاربر، استفاده از عنوان زبان در آدرس صفحات درخواستی و یا تعیین زبان توسط کاربر در تنظیمات برنامه/سایت و ذخیره آن در کوکی یا دیتابیس و مواردی از این دست روشهایی است که معمولا برای تعیین زبان جاری از آن استفاده میشود. در کدهای نمونه ای که در بخشهای قبل آورده شده است فرض شده است که زبان جاری سیستم درون یک کوکی ذخیره میشود بنابراین برای استفاده از این روش میتوان از قطعه کدی مشابه زیر (مثلا در فایل Layout.cshtml_) برای تعیین و تغییر زبان استفاه کرد:
<select id="langs" onchange="languageChanged()">
  <option value="fa-IR">فارسی</option>
  <option value="en-US">انگلیسی</option>
</select>
<script type="text/javascript">
  function languageChanged() {
    setCookie("MyLanguageCookieName", $('#langs').val(), 365);
    window.location.reload();
  }
  document.ready = function () {
    $('#langs').val(getCookie("MyLanguageCookieName"));
  };
  function setCookie(name, value, exdays, path) {
    var exdate = new Date();
    exdate.setDate(exdate.getDate() + exdays);
    var newValue = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + ((path == null) ? "" : "; path=" + path) ;
    document.cookie = name + "=" + newValue;
  }
  function getCookie(name) {
    var i, x, y, cookies = document.cookie.split(";");
    for (i = 0; i < cookies.length; i++) {
      x = cookies[i].substr(0, cookies[i].indexOf("="));
      y = cookies[i].substr(cookies[i].indexOf("=") + 1);
      x = x.replace(/^\s+|\s+$/g, "");
      if (x == name) {
        return unescape(y);
      }
    }
  }
</script> 
متدهای setCookie و getCookie جاوا اسکریپتی در کد بالا از اینجا گرفته شده اند البته پس از کمی تغییر.
نکته: مطلب Cookieها بحثی نسبتا مفصل است که در جای خودش باید به صورت کامل آورده شود. اما در اینجا تنها به همین نکته اشاره کنم که عدم توجه به پراپرتی path کوکی‌ها در این مورد خاص برای خود من بسیار گیج‌کننده و دردسرساز بود. 
به عنوان راهی دیگر میتوان به جای روش ساده استفاده از کوکی، تنظیماتی در اختیار کاربر قرار داد تا بتواند زبان تنظیم شده را درون یک فایل یا دیتابیس ذخیره کرد البته با درنظر گرفتن مسائل مربوط به کش کردن این تنظیمات.
راه حل بعدی میتواند استفاده از تنظیمات مرورگر کاربر برای دریافت زبان جاری تنظیم شده است. مرورگرها تنظیمات مربوط به زبان را در قسمت Accept-Languages در HTTP Header درخواست ارسالی به سمت سرور قرار میدهند. بصورت زیر:
GET https://www.dntips.ir HTTP/1.1
...
Accept-Language: fa-IR,en-US;q=0.5
...
این هم تصویر مربوط به Fiddler آن:

نکته: پارامتر q در عبارت مشخص شده در تصویر فوق relative quality factor نام دارد و به نوعی مشخص کننده اولویت زبان مربوطه است. مقدار آن بین 0 و 1 است و مقدار پیش فرض آن 1 است. هرچه مقدار این پارامتر بیشتر باشد زبان مربوطه اولویت بالاتری دارد. مثلا عبارت زیر را درنظر بگیرید:
Accept-Language: fa-IR,fa;q=0.8,en-US;q=0.5,ar-BH;q=0.3
در این حالت اولویت زبان fa-IR برابر 1 و fa برابر 0.8 (fa;q=0.8) است. اولویت دیگر زبانهای تنظیم شده نیز همانطور که نشان داده شده است در مراتب بعدی قرار دارند. در تنظیم نمایش داده شده برای تغییر این تنظیمات در IE میتوان همانند تصویر زیر اقدام کرد:

در تصویر بالا زبان فارسی اولویت بالاتری نسبت به انگلیسی دارد. برای اینکه سیستم g11n دات نت به صورت خودکار از این مقادیر جهت زبان ثرد جاری استفاده کند میتوان از تنظیم زیر در فایل کانفیگ استفاده کرد:
<system.web>
    <globalization enableClientBasedCulture="true" uiCulture="auto" culture="auto"></globalization>
</system.web>
در سمت سرور نیز برای دریافت این مقادیر تنظیم شده در مرورگر کاربر میتوان از کدهای زیر استفاه کرد. مثلا در یک اکشن فیلتر:
var langs = filterContext.HttpContext.Request.UserLanguages;
پراپرتی UserLanguages از کلاس Request حاوی آرایه‌ای از استرینگ است. این آرایه درواقع از Split کردن مقدار Accept-Languages با کاراکتر ',' بدست می‌آید. بنابراین اعضای این آرایه رشته‌ای از نام زبان به همراه پارامتر q مربوطه خواهند بود (مثل "fa;q=0.8").
راه دیگر مدیریت زبانها استفاده از عنوان زبان در مسیر درخواستی صفحات است. مثلا آدرسی شبیه به www.MySite.com/fa/Employees نشان میدهد کاربر درخواست نسخه فارسی از صفحه Employees را دارد. نحوه استفاده از این عناوین و نیز موقعیت فیزیکی این عناوین در مسیر صفحات درخواستی کاملا به سلیقه برنامه نویس و یا کارفرما بستگی دارد. روش کلی بهره برداری از این روش در تمام موارد تقریبا یکسان است.
برای پیاده سازی این روش ابتدا باید یک route جدید در فایل Global.asax.cs اضافه کرد:
routes.MapRoute(
    "Localization", // Route name
    "{lang}/{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
دقت کنید که این route باید قبل از تمام routeهای دیگر ثبت شود. سپس باید کلاس پایه کنترلر را به صورت زیر پیاده سازی کرد:
public class BaseController : Controller
{
  protected override void ExecuteCore()
  {
    var lang = RouteData.Values["lang"];
    if (lang != null && !string.IsNullOrWhiteSpace(lang.ToString()))
    {
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang.ToString());
    }
    base.ExecuteCore();
  }
}
این کار را در یک اکشن فیلتر هم میتوان انجام داد اما با توجه به توضیحاتی که در قسمت قبل داده شد استفاده از اکشن فیلتر برای تعیین زبان جاری کار مناسبی نیست.
نکته: به دلیل آوردن عنوان زبان در مسیر درخواستها باید کتترل دقیقتری بر کلیه مسیرهای موجود داشت!

استفاده از ویوهای جداگانه برای زبانهای مختلف
برای اینکار ابتدا ساختار مناسبی را برای نگهداری از ویوهای مختلف خود درنظر بگیرید. مثلا میتوانید همانند نامگذاری فایلهای Resource از نام زبان یا کالچر به عنوان بخشی از نام فایلهای ویو استفاده کنید و تمام ویوها را در یک مسیر ذخیره کنید. همانند تصویر زیر:

البته اینکار ممکن است به مدیریت این فایلها را کمی مشکل کند چون به مرور زمان تعداد فایلهای ویو در یک فولدر زیاد خواهد شد. روش دیگری که برای نگهداری این ویوها میتوان به کار برد استفاده از فولدرهای جداگانه با عناوین زبانهای موردنظر است. مانند تصویر زیر:

روش دیگری که برای نگهداری و مدیریت بهتر ویوهای زبانهای مختلف از آن استفاده میشود به شکل زیر است:

استفاه از هرکدام از این روشها کاملا به سلیقه و راحتی مدیریت فایلها برای برنامه نویس بستگی دارد. درهر صورت پس از انتخاب یکی از این روشها باید اپلیکشن خود را طوری تنظیم کنیم که با توجه به زبان جاری سیستم، ویوی مربوطه را جهت نمایش انتخاب کند.
مثلا برای روش اول نامگذاری ویوها میتوان از روش دستکاری متد OnActionExecuted در کلاس پایه کنترلر استفاده کرد:
public class BaseController : Controller
{
  protected override void OnActionExecuted(ActionExecutedContext context)
  {
    var view = context.Result as ViewResultBase;
    if (view == null) return; // not a view
    var viewName = view.ViewName;
    view.ViewName = GetGlobalizationViewName(viewName, context);
    base.OnActionExecuted(context);
  }
  private static string GetGlobalizationViewName(string viewName, ControllerContext context)
  {
    var cultureName = Thread.CurrentThread.CurrentUICulture.Name;
    if (cultureName == "en-US") return viewName; // default culture
    if (string.IsNullOrEmpty(viewName))
      return context.RouteData.Values["action"] + "." + cultureName; // "Index.fa"
    int i;
    if ((i = viewName.IndexOf('.')) > 0) // ex: Index.cshtml
      return viewName.Substring(0, i + 1) + cultureName + viewName.Substring(i); // "Index.fa.cshtml"
    return viewName + "." + cultureName; // "Index" ==> "Index.fa"
  }
}
همانطور که قبلا نیز شرح داده شد، چون متد ExecuteCore قبل از OnActionExecuted صدا زده میشود بنابراین از تنظیم درست مقدار کالچر در ثرد جاری اطمینان داریم.
روش دیگری که برای مدیریت انتخاب ویوهای مناسب استفاده از یک ویوانجین شخصی سازی شده است. مثلا برای روش سوم نامگذاری ویوها میتوان از کد زیر استفاده کرد:
public sealed class RazorGlobalizationViewEngine : RazorViewEngine
  {
    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
      return base.CreatePartialView(controllerContext, GetGlobalizationViewPath(controllerContext, partialPath));
    }
    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
      return base.CreateView(controllerContext, GetGlobalizationViewPath(controllerContext, viewPath), masterPath);
    }
    private static string GetGlobalizationViewPath(ControllerContext controllerContext, string viewPath)
    {
      //var controllerName = controllerContext.RouteData.GetRequiredString("controller");
      var request = controllerContext.HttpContext.Request;
      var lang = request.Cookies["MyLanguageCookie"];
      if (lang != null && !string.IsNullOrEmpty(lang.Value) && lang.Value != "en-US")
      {
        var localizedViewPath = Regex.Replace(viewPath, "^~/Views/", string.Format("~/Views/Globalization/{0}/", lang.Value));
        if (File.Exists(request.MapPath(localizedViewPath))) viewPath = localizedViewPath;
      }
      return viewPath;
    }
و برای ثبت این ViewEngine در فایل Global.asax.cs خواهیم داشت:
protected void Application_Start()
{
  ViewEngines.Engines.Clear();
  ViewEngines.Engines.Add(new RazorGlobalizationViewEngine());
}

محتوای یک فایل Resource
ساختار یک فایل resx. به صورت XML استاندارد است. در زیر محتوای یک نمونه فایل Resource با پسوند resx. را مشاهده میکنید:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema ...
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
   ...
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="RightToLeft" xml:space="preserve">
    <value>false</value>
    <comment>RightToleft is false in English!</comment>
  </data>
</root>
در قسمت ابتدایی تمام فایلهای resx. که توسط ویژوال استودیو تولید میشود کامنتی طولانی وجود دارد که به صورت خلاصه به شرح محتوا و ساختار یک فایل Resource میپردازد. در ادامه تگ نسبتا طولانی xsd:schema قرار دارد. از این قسمت برای معرفی ساختار داده ای فایلهای XML استفاده میشود. برای آشنایی بیشتر با XSD (یا XML Schema) به اینجا مراجعه کنید. به صورت خلاصه میتوان گفت که XSD برای تعیین ساختار داده‌ها یا تعیین نوع داده ای اطلاعات موجود در یک فایل XML به کار میرود. درواقع تگهای XSD به نوعی فایل XML ما را Strongly Typed میکند. با توجه به اطلاعات این قسمت، فایلهای resx. شامل 4 نوع گره اصلی هستند که عبارتند از: metadata و assembly و data و resheader. در تعریف هر یک از گره‌ها در این قسمت مشخصاتی چون نام زیر گره‌های قابل تعریف در هر گره و نام و نوع خاصیتهای هر یک معرفی شده است.
بخش موردنظر ما در این مطلب قسمت انتهایی این فایلهاست (تگهای resheader و data). همانطور در بالا مشاهده میکنید تگهای reheader شامل تنظیمات مربوط به فایل resx. با ساختاری ساده به صورت name/value است. یکی از این تنظیمات resmimetype فایل resource را معرفی میکند که درواقع مشخص کننده نوع محتوای (Content Type) فایل XML است(^). برای فایلهای resx این مقدار برابر text/microsoft-resx است. تنظیم بعدی نسخه مربوط به فایل resx (یا Microsoft ResX Schema) را نشان میدهد. در حال حاضر نسخه جاری (در VS 2010) برابر 2.0 است. تنظیم بعدی مربوط به کلاسهای reader و writer تعریف شده برای استفاده از این فایلهاست. به نوع این کلاسهای خواننده و نویسنده فایلهای resx. و مکان فیزیکی و فضای نام آنها دقت کنید که در مطالب بعدی از آنها برای ویرایش و بروزرسانی فایلهای resource در زمان اجرا استفاده خواهیم کرد.
در پایان نیز تگهای data که برای نگهداری داده‌ها از آنها استفاده میشود. هر گره data شامل یک خاصیت نام (name) و یک زیرگره مقدار (value) است. البته امکان تعیین یک کامنت در زیرگره comment نیز وجود دارد که اختیاری است. هر گره data مینواند شامل خاصیت type و یا mimetype نیز باشد. خاصیت type مشخص کننده نوعی است که تبدیل text/value را با استفاده از ساختار TypeConverter پشتیبانی میکند. البته اگر در نوع مشخص شده این پشتیبانی وجود نداشته باشد، داده موردنظر پس از سریالایز شدن با فرمت مشخص شده در خاصیت mimetype ذخیره میشود. این mimetype اطلاعات موردنیاز را برای کلاس خواننده این فایلها (ResXResourceReader به صورت پیشفرض) جهت چگونگی بازیابی آبجکت موردنظر فراهم میکند. مشخص کردن این دو خاصیت برای انواع رشته ای نیاز نیست. انواع mimetype قابل استفاده عبارتند از:
- application/x-microsoft.net.object.binary.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Binary.BinaryFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود (راجع به انکدینگ base64 ^ و ^).
- application/x-microsoft.net.object.soap.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Soap.SoapFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
- application/x-microsoft.net.object.bytearray.base64: آبجکت ابتدا باید با استفاده از یک System.ComponentModel.TypeConverter به آرایه ای از بایت سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
نکته: امکان جاسازی کردن (embed) فایلهای resx. در یک اسمبلی یا کامپایل مستقیم آن به یک سَتِلایت اسمبلی (ترجمه مناسبی برای satellite assembly پیدا نکردم، چیزی شبیه به اسمبلی قمری یا وابسته و از این قبیل ...) وجود ندارد. ابتدا باید این فایلهای resx. به فایلهای resources. تبدیل شوند. اینکار با استفاده از ابزار Resource File Generator (نام فایل اجرایی آن resgen.exe است) انجام میشود (^ و ^). سپس میتوان با استفاده از Assembly Linker ستلایت اسمبلی مربوطه را تولید کرد (^). کل این عملیات در ویژوال استودیو با استفاده از ابزار msbuild به صورت خودکار انجام میشود!

نحوه یافتن کلیدهای Resource در بین فایلهای مختلف Resx توسط پرووایدر پیش فرض در دات نت
عملیات ابتدا با بررسی خاصیت CurrentUICulture از ثرد جاری آغاز میشود. سپس با استفاده از عنوان استاندارد کالچر جاری، فایل مناسب Resource یافته میشود. در نهایت بهترین گزینه موجود برای کلید درخواستی از منابع موجود انتخاب میشود. مثلا اگر کالچر جاری fa-IR و کلید درخواستی از کلاس Texts باشد ابتدا جستجو برای یافتن فایل Texts.fa-IR.resx آغاز میشود و اگر فایل موردنظر یا کلید درخواستی در این فایل یافته نشد جستجو در فایل Texts.fa.resx ادامه می‌یابد. اگر باز هم یافته نشد درنهایت این عملیات جستجو در فایل resource اصلی خاتمه می‌یابد و مقدار کلید منبع پیش فرض به عنوان نتیجه برگشت داده میشود. یعنی در تمامی حالات سعی میشود تا دقیقترین و بهترین و نزدیکترین نتیجه انتخاب شود. البته درصورتیکه از یک پرووایدر شخصی سازی شده برای کار خود استفاده میکنید باید چنین الگوریتمی را جهت یافتن کلیدهای منابع خود از فایلهای Resource (یا هرمنبع دیگر مثل دیتابیس یا حتی یک وب سرویس) درنظر بگیرید.

Globalization در کلاینت (javascript g11n)
یکی دیگر از موارد استفاده g11n در برنامه نویسی سمت کلاینت است. با وجود استفاده گسترده از جاوا اسکریپت در برنامه نویسی سمت کلاینت در وب اپلیکیشنها، متاسفانه تا همین اواخر عملا ابزار یا کتابخانه مناسبی برای مدیریت g11n در این زمینه وجود نداشته است. یکی از اولین کتابخانه‌های تولید شده در این زمینه کتابخانه jQuery Globalization است که توسط مایکروسافت توسعه داده شده است (برای آشنایی بیشتر با این کتابخانه به ^ و ^ مراجعه کنید). این کتابخانه بعدا تغییر نام داده و اکنون با عنوان Globalize شناخته میشود. Globalize یک کتابخانه کاملا مستقل است که وابستگی به هیچ کتابخانه دیگر ندارد (یعنی برای استفاده از آن نیازی به jQuery نیست). این کتابخانه حاوی کالچرهای بسیاری است که عملیات مختلفی چون فرمت و parse انواع داده‌ها را نیز در سمت کلاینت مدیریت میکند. همچنین با فراهم کردن منابعی حاوی جفتهای key/culture میتوان از مزایایی مشابه مواردی که در این مطلب بحث شد در سمت کلاینت نیز بهره برد. نشانی این کتابخانه در github اینجا است. با اینکه خود این کتابخانه ابزار کاملی است اما در بین کالچرهای موجود در فایلهای آن متاسفانه پشتیبانی کاملی از زبان فارسی نشده است. ابزار دیگری که برای اینکار وجود دارد پلاگین jquery localize است که برای بحث g11n رشته‌ها پیاده‌سازی بهتر و کاملتری دارد.

در مطالب بعدی به مباحث تغییر مقادیر کلیدهای فایلهای resource در هنگام اجرا با استفاده از روش مستقیم تغییر محتوای فایلها و کامپایل دوباره توسط ابزار msbuild و نیز استفاده از یک ResourceProvider شخصی سازی شده به عنوان یک راه حل بهتر برای اینکار میپردازم.
در تهیه این مطلب از منابع زیر استفاده شده است: