مطالب
ایجاد سرویس Account Manager با تکنولوژی های Identity 2.1 و Web API 2.2
ASP.NET Identity 2.1 جدیدترین فریم ورک عضویت و مدیریت کاربر است که چندی پیش توسط شرکت مایکروسافت منتشر شد. این سیستم عضویت می‌تواند به تمامی فریم‌ورک‌های دات نتی مانند Web API، MVC و ... متصل گردد.
در این دوره چند قسمتی به همراه یک پروژه‌ی نمونه، نحوه‌ی ارتباط Identity و Web API را نمایش خواهیم داد. در قسمت front-end این پروژه‌ی SPA، ما از AngularJs استفاده خواهیم نمود. قسمت front-end که توسط AngularJs توسعه داده می‌شود از bearet token based authentication استفاده می‌کند. این متد از JSON Web Token برای فرایند Authorization بهره می‌گیرد که به اختصار آن را JWT نیز می‌نامند. این روش سیستم اعتبار سنجی role based بوده و  تمامی آنچه را که در یک سیستم membership داریم، پوشش می‌دهد. توجه داشته باشید که برای فهم بهتر تمامی مراحل، ما پروژه را بدون هیچ قالب از پیش تعریف شده‌ای در VS2013 آغاز می‌کنیم.
به دلیل اینکه پروژه در طی چند قسمت انجام می‌پذیرد آن را به بخش‌های زیر تقسیم می‌کنیم.
  1.  تنظیمات اولیه ASP.NET Identity 2.1 با Web API
  2.  ایجاد Account Confirmation به وسیله Identity به همراه تنظیمات policy برای user name و password
  3.  توسعه OAuth Json Web Token Authentication به کمک Web API و Identity
  4.  ایجاد یک سیستم Role Based و تایید صلاحیت‌های مربوط به آن
  5.  توسعه Web API Claims Authorization در Identity 2.1
  6.  توسعه بخش front-end با AngularJs

تنظیمات اولیه ASP.NET Identity 2.1 با Web API 

1. تنظیمات ASP.Net Identity 2.1

1-1. ساخت یک پروژه Web API

در ابتدا ما یک empty solution را با نام "AspNetIdentity" همانند شکل مقابل می‌سازیم.

یک ASP.NET Web Application با نام "AspNetIdentity.WebApi" را به این solution اضافه می‌نماییم. در بخش select template ما empty template را انتخاب می‌کنیم. همچنین در قسمت add folders and core references for: نیز هیچیک از گزینه‌ها را انتخاب نمی‌کنیم.

1-2. نصب package‌های مورد نیاز از Nuget

در زیر package‌های مورد نیاز برای ASP.NET Web API و Owin را مشاهده میکنید. همچنین package‌های مربوط به ASP.Net Identity 2.1 نیز در زیر قرار داده شده‌اند.

Install-Package Microsoft.AspNet.Identity.Owin -Version 2.1.0
Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.1.0
Install-Package Microsoft.Owin.Host.SystemWeb -Version 3.0.0
Install-Package Microsoft.AspNet.WebApi.Owin -Version 5.2.2
Install-Package Microsoft.Owin.Security.OAuth -Version 3.0.0
Install-Package Microsoft.Owin.Cors -Version 3.0.0

1-3. اضافه کردن user class و database context

حال که تمامی پکیج‌های مورد نیاز را به پروژه خود اضافه نمودیم، قصد داریم تا اولین کلاس EF را با نام "ApplicationUser" به پروژه اضافه کنیم. این کلاس، کاربری را که قصد ثبت نام در membership system، دارد را نمایش می‌دهد. برای این کار ما یک کلاس جدید را به نام "ApplicationUser" می‌سازیم و کلاس "Microsoft.AspNet.Identity.EntityFramework.IdentityUser" را در آن به ارث می‌بریم.

برای این کار ما یک پوشه‌ی جدید را در برنامه با نام "Infrastructure" می‌سازیم و درون آن کلاس ذکر شده را اضافه می‌کنیم:

 public class ApplicationUser : IdentityUser
    {
        [Required]
        [MaxLength(100)]
        public string FirstName { get; set; }
 
        [Required]
        [MaxLength(100)]
        public string LastName { get; set; }
 
        [Required]
        public byte Level { get; set; }
 
        [Required]
        public DateTime JoinDate { get; set; }
 
    }

پس از افزودن کلاس User، نوبت به اضافه نمودن Db Context است. این کلاس وظیفه‌ی ارتباط با پایگاه داده را بر عهده دارد. ما یک کلاس جدید را با نام ApplicationDbContext، به پوشه‌ی Infrastructure اضافه می‌نماییم. کد مربوط به این کلاس به صورت زیر است:

 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
            Configuration.ProxyCreationEnabled = false;
            Configuration.LazyLoadingEnabled = false;
        }
 
        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
 
    }

همانطور که ملاحظه می‌کنید این کلاس از IdentityDbContext ارث بری نموده است. این کلاس یک نسخه‌ی جدیدتر از DbContext است که تمامی نگاشت‌های Entity Framework Code First را انجام می‌دهد. در ادامه ما یک Connection String را با نام DefaultConnection در فایل web.config اضافه می‌نماییم.

همچنین متد static ایی را با نام Create که در تکه کد فوق از سوی Owin Startup class فراخوانی می‌گردد که در ادامه به شرح آن نیز خواهیم پرداخت.

ConnectionString قرار داده شده در فایل web.config در قسمت زیر قرار داده شده است:

<connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=.\sqlexpress;Initial Catalog=AspNetIdentity;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
  </connectionStrings>

قدم 4: ساخت پایگاه داده و فعال سازی DB Migration

حال ما باید EF CodeFirst Migration را برای آپدیت کردن دیتابیس، بجای دوباره ساختن آن به ازای هر تغییری، فعال نماییم. برای این کار بایستی در قسمت NuGet Package Manager Console عبارت زیر را وارد نماییم:

enable-migrations
add-migration InitialCreate

اگر تا به اینجای کار تمامی مراحل به درستی صورت گرفته باشند، پوشه‌ی Migration با موفقیت ساخته می‌شود. همچنین پایگاه داده متشکل از جداول مورد نیاز برای سیستم Identity نیز ایجاد می‌شود. برای اطلاعات بیشتر می‌توانید مقالات مرتبط با Code First را مطالعه نمایید. ساختار جداول ما باید به صورت زیر باشد:

در بخش بعدی، کلاس‌های مربوط به UserManager را ایجاد خواهیم کرد و پس از آن فایل Startup Owin را برای مدیریت کاربران، تشریح می‌کنیم.

مسیرراه‌ها
WPF
          مطالب
          اجزاء معماری سیستم عامل اندروید (قسمت دوم معماری امنیتی اندروید) :: بخش چهارم
          ایمن کردن برنامه تولید شده در برابر حملات:

          هنگام دریافت اطلاعات از کاربر، باید داده‌ها در جایی ذخیره شوند. اینکه داده‌ها در کجا ذخیره و نگه داری شوند و از نفوذ به آنها جلوگیری شود، نهایت امن بودن برنامه شما را نشان میدهد. باید فرض کنید که برنامه شما به طور مستقیم یا غیرمستقیم در برخی موارد مورد حمله قرار میگیرد و تنها چیزی که بین حفاظت از اطلاعات کاربر نهایی، شما و حفاظت از داده‌ها مطرح می‌شود برعهده شما خواهد بود. چند نمونه از حملات را توضیح خواهم داد:
          حملات غیر مستقیم (Indirect Attacks)
          قبل از اینکه این بحث را باز کنم اجازه دهید ببینیم ترس از یک حمله تا چه حد استرس بوجود می‌آورد؟ در نیمه دوم 2010 و اوایل 2011 ، دو آسیب‌پذیری خطرناک در نسخه‌های Android به ترتیب 2.2 و 2.3 کشف شدند. آسیب‌پذیری در اصل همان خطری است که در آن یک مهاجم می‌تواند هر پرونده‌ای را که بر روی SD دستگاه ذخیره شده‌است کپی کند و به سرقت ببرد! حمله بدون اجازه از حافظه اتفاق افتاده است و آسیب پذیری، این راه را برای آن فراهم کرده است. به تصویر دقت کنید! ( این آسیب پذیری مربوط به سرقت اطلاعات است)

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

          1. یک کاربر از یک وب سایت حاوی کدهای مخرب و آلوده که یک فایل را میزبانی می‌کند، مانند evil.html بازدید می‌کند.
          2. با توجه به یک بخش از آسیب پذیری، فایل evil.html دانلود می‌شود و کارت SD آن را بدون هشدار به کاربر، ذخیره می‌کند!
          3. با توجه به بخش دیگری از آسیب پذیری، به محض اینکه فایل ذخیره شد، می‌توان کدهای جاوا اسکریپت مخرب را روی آن اجرا کرد!
          4. بدلیل بخش پایانی حمله روی این آسیب پذیری، کدهای جاوااسکریپت تحت شرایطی خاص روی سیستم محلی (local) اجرا می‌شوند! ابزار آلوده و برنامه نویسی شده، براحتی روی کارت SD ذخیره و مستقر شده و به وب سایت مهاجم برای ارسال اطلاعات قربانی دسترسی کامل دارد.
          فرض کنید که برنامه شما تمام اطلاعات ذخیره‌شده در کارت SD را برای ذخیره‌سازی، زیر نظر دسترسی‌های خودش ذخیره کرده است و مطابق با الگوی دایرکتوری‌ها جلو می‌رود. با اینحال داده‌های شما در معرض خطر سرقت قرار گرفته‌اند! این مثالی از حمله غیرمستقیم به برنامه شما است. اینکه اپلیکیشن شما تا چه میزان در برابر حملات غیرمستقیم مقاوم خواهد بود، بستگی به تلاش شما در برنامه نویسی و تحلیل موارد امنیتی دارد و لازمه آن این است که قبل از تولید نرم افزار حتما تحلیل امنیتی بعمل آید و اپلیکیشن را از آنها منع کنید.
          ممکن است بپرسید، "من فقط یک توسعه دهنده اندرویدی کوچکی هستم که قصد دارم برنامه خودم را در یک مارکت اندرویدی بفروشم و قیمت این برنامه خیلی پایین است. بنابراین آیا واقعا باید زمانی را برای انجام این کار امنیتی از قبل هدر دهم؟

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

          حملات مستقیم (Direct Attacks) 

          حملات مستقیم به طور قابل‌توجهی متفاوت هستند و می‌توانند شکل‌های گوناگونی برای حمله داشته باشند. یک حمله مستقیم می‌تواند به عنوان فردی که مستقیما در برنامه شما هدف قرار داده می‌شود، طبقه‌بندی شود. بنابراین، مهاجم به دنبال آسیب پذیری برای نفوذ در طراحی برنامه شما برای جمع‌آوری اطلاعات حساس در مورد شما و کاربران‌تان است. استفاده از کاربران برنامه و یا حمله به کارگزار، از مواردی است که یک مهاجم در اولویت کار خود قرار می‌دهد! یک مهاجم ممکن است به دنبال برنامه‌های کاربردی تلفن همراه باشد که متعلق به یک نهاد دولتی است مثل، یک بانک خاص که شما اپلیکیشن آنرا روی تلفن خود نصب کرده‌اید و آپدیت‌های امنیتی راهم انجام نداده‌اید! اگر طرح امنیتی روی برنامه ضعیف باشد و داده‌های حساس و حیاتی کاربران در محلی امن نگه داری نشود یا حتی ارتباط بین برنامه و ssl و سرور بدرستی برقرار نباشد، مهاجم میتواند حملات خاصی را روی ssl انجام دهد و نقاط ضعف را شناسایی کرده و در کسری از زمان به سرور متصل شود! این حمله یک حمله مستقیم و بدون دخالت فایل یا ابزار خاصی است. این یک حمله مستقیم به یک برنامه خاص است.

          Proxim و ذخیره داده 

          بیایید با یک مثال ساده با نام Proxim شروع کنیم: برای نوشتن یک برنامه که می‌تواند یک SMS را به افراد خاص و معین ارسال کند، قرار دادی بسته‌ایم؛ با توجه به نزدیکی به مجموعه‌ای از مختصات مکانی آنها روی GPS. برای مثال: کاربر در این برنامه می‌تواند شماره تماس همسر خود را ذخیره کرده و هر زمان که به فاصله 3 مایلی به خانه یا محل کار برسد، با او تماس میگیرد. بدین صورت همسر فرد مطلع می‌شود که او نزدیک به محل کار یا خانه است و با یک تماس تلفنی او را آگاه می‌سازد.

          کدهای زیر بخشی از فایل (Save Routine, SaveController. java) هستند:

          package net.zenconsult.android.controller;
          
          import java.io.File;
          import java.io.FileNotFoundException;
          import java.io.FileOutputStream;
          import java.io.IOException;
          import net.zenconsult.android.model.Contact;
          import net.zenconsult.android.model.Location;
          import android.content.Context;
          import android.os.Environment;
          import android.util.Log;
          public class SaveController {
           private static final String TAG = "SaveController";
           public static void saveContact(Context context, Contact contact) {
            if (isReadWrite()) {
             try {
              File outputFile = new File(context.getExternalFilesDir(null), contact.getFirstName());
              FileOutputStream outputStream = new FileOutputStream(outputFile);
              outputStream.write(contact.getBytes());
              outputStream.close();
             } catch (FileNotFoundException e) {
              Log.e(TAG, "File not found");
             } catch (IOException e) {
              Log.e(TAG, "IO Exception");
             }
            } else {
             Log.e(TAG, "Error opening media card in read/write mode!");
            }
           }
           public static void saveLocation(Context context, Location location) {
            if (isReadWrite()) {
             try {
              File outputFile = new File(context.getExternalFilesDir(null), location.getIdentifier());
              FileOutputStream outputStream = new FileOutputStream(outputFile);
              outputStream.write(location.getBytes());
              outputStream.close();
             } catch (FileNotFoundException e) {
              Log.e(TAG, "File not found");
             } catch (IOException e) {
              Log.e(TAG, "IO Exception");
             }
            } else {
             Log.e(TAG, "Error opening media card in read/write mode!");
            }
           }
           private static boolean isReadOnly() {
            Log.e(TAG, Environment
             .getExternalStorageState());
            return Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment
             .getExternalStorageState());
           }
           private static boolean isReadWrite() {
            Log.e(TAG, Environment
             .getExternalStorageState());
            return Environment.MEDIA_MOUNTED.equals(Environment
             .getExternalStorageState());
           }
          }
          هر بار که کاربر دکمه ذخیره مکان یا دکمه ذخیره تماس را انتخاب می‌کند، عملیات صورت میگیرد. حال بیایید به کدهای مکان و تماس نگاهی بیندازیم:

            کدهای فایل ( Location .java)

          package net.zenconsult.android.model;
          publicclass Location {
           private String identifier;
           privatedouble latitude;
           privatedouble longitude;
           public Location() {}
           publicdouble getLatitude() {
            return latitude;
           }
           publicvoid setLatitude(double latitude) {
            this.latitude = latitude;
           }
           publicdouble getLongitude() {
            return longitude;
           }
           publicvoid setLongitude(double longitude) {
            this.longitude = longitude;
           }
           publicvoid setIdentifier(String identifier) {
            this.identifier = identifier;
           }
           public String getIdentifier() {
            return identifier;
           }
           public String toString() {
            StringBuilder ret = new StringBuilder();
            ret.append(getIdentifier());
            ret.append(String.valueOf(getLatitude()));
            ret.append(String.valueOf(getLongitude()));
            return ret.toString();
           }
           publicbyte[] getBytes() {
            return toString().getBytes();
           }
          }

          کدهای فایل (Contact.java)

          package net.zenconsult.android.model;
          publicclass Contact {
           private String firstName;
           private String lastName;
           private String address1;
           private String address2;
           private String email;
           private String phone;
           public Contact() {}
           public String getFirstName() {
            return firstName;
           }
           publicvoid setFirstName(String firstName) {
            this.firstName = firstName;
           }
           public String getLastName() {
            return lastName;
           }
           publicvoid setLastName(String lastName) {
            this.lastName = lastName;
           }
           public String getAddress1() {
            return address1;
           }
           publicvoid setAddress1(String address1) {
            this.address1 = address1;
           }
           public String getAddress2() {
            return address2;
           }
           publicvoid setAddress2(String address2) {
            this.address2 = address2;
           }
           public String getEmail() {
            return email;
           }
           publicvoid setEmail(String email) {
            this.email = email;
           }
           public String getPhone() {
            return phone;
           }
           publicvoid setPhone(String phone) {
            this.phone = phone;
           }
           public String toString() {
            StringBuilder ret = new StringBuilder();
            ret.append(getFirstName() + "|");
            ret.append(getLastName() + "|");
            ret.append(getAddress1() + "|");
            ret.append(getAddress2() + "|");
            ret.append(getEmail() + "|");
            ret.append(getPhone() + "|");
            return ret.toString();
           }
           publicbyte[] getBytes() {
            return toString().getBytes();
           }
          }
          کلاس‌های مکان و تماس با استاندارد یکسانی طراحی شده‌اند و داده‌ها در محلی امن، نگه‌داری می‌شوند. هر کدام از آنها شامل ()toString و ()getBytes است که کل محتوای کلاس را به عنوان یک رشته یا مجموعه‌ای از بایت‌ها برمیگرداند.
          اگر بنا بود تا به صورت دستی یک شئ تماس را اضافه کنیم، احتمالا باید به صورت زیر عمل میکردیم:
          final Contact contact = new Contact();
          contact.setFirstName("User 1");
          contact.setLastName("L1");
          contact.setAddress1("");
          contact.setAddress2("");
          contact.setEmail("name@site.net");
          contact.setPhone("12120031337");

          مطالب
          بومی سازی منابع در پروژه‌های ASP.NET Core Web API
          اگر پروژه‌ی ما فقط از یک Web API تشکیل شده و نیاز است در قسمت‌های مختلف آن، مانند کنترلرها، سرویس‌ها، اعتبارسنج‌ها و غیره از منابع بومی شده استفاده شود، می‌توان از یک راه حل ساده‌ی «SharedResource» استفاده کرد؛ با این مزایا و شرایط:
           - تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار می‌گیرند. این فایل نیز در یک اسمبلی مستقل از برنامه‌ی اصلی اضافه می‌شود.
           - با استفاده از تزریق سرویس IStringLocalizer می‌توان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامه‌ی Web API دسترسی یافت.
           - در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ می‌شود.
           - کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx می‌باشند.
           
          در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی می‌کنیم.
           
           
          قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources

          پس از ایجاد پروژه‌ی ابتدایی Web API به نام Core3xSharedResource.WebApi، یک اسمبلی جدید را برای مثال به نام Core3xSharedResource.ExternalResources تعریف کرده و در داخل آن پوشه‌ی جدید Resources را تعریف می‌کنیم. به این پوشه، فایل منبع جدیدی را به نام SharedResource.fa.resx اضافه می‌کنیم. در کنار آن باید یک کلاس خالی به نام SharedResource.cs نیز وجود داشته باشد.

          کار با ین فایل (و یا فایل‌های دیگری مانند SharedResource.en.resx) همانند تمام فایل‌های منبع استاندارد است و نکته‌ی خاصی را به همراه ندارد.


          معرفی فایل منبع اشتراکی به سرویس‌های بومی سازی برنامه

          پس از ایجاد و تکمیل فایل منبع اشتراکی، برای معرفی آن به برنامه، ابتدا کلاس جدید LocalizationConfig را تعریف کرده و در آن متد جدید AddCustomLocalization را به صورت زیر معرفی می‌کنیم:
              public static class LocalizationConfig
              {
                  public static IMvcBuilder AddCustomLocalization(this IMvcBuilder mvcBuilder, IServiceCollection services)
                  {
                      mvcBuilder.AddDataAnnotationsLocalization(options =>
                              {
                                  const string resourcesPath = "Resources";
                                  string baseName = $"{resourcesPath}.{nameof(SharedResource)}";
                                  var location = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName).Name;
          
                                  options.DataAnnotationLocalizerProvider = (type, factory) =>
                                  {
                                      // to use `SharedResource.fa.resx` file
                                      return factory.Create(baseName, location);
                                  };
                              });
          
                      services.AddLocalization();
                      services.AddScoped<IStringLocalizer>(provider =>
                                      provider.GetRequiredService<IStringLocalizer<SharedResource>>());
          
                      services.AddScoped<ISharedResourceService, SharedResourceService>();
                      return mvcBuilder;
                  }
              }
          - در اینجا در ابتدا توسط متد AddDataAnnotationsLocalization، کار معرفی اسمبلی ثالثی که باید تعاریف بومی سازی را از آن دریافت کرد، صورت گرفته‌است.
          - سپس با استفاده از متد AddLocalization، سرویس‌های پایه‌ی بومی سازی ASP.NET Core به برنامه اضافه می‌شوند. برای مثال پس از این تعریف اگر در هر جائی از برنامه سرویس <IStringLocalizer<SharedResource را تزریق کنید، می‌توان به مداخل فایل منبع اشتراکی، دسترسی یافت.
          - در ادامه امکان تزریق سرویس غیرجنریک IStringLocalizer را نیز میسر کرده‌ایم که تعاریف خودش را از همان سرویس توکار <IStringLocalizer<SharedResource دریافت می‌کند. مزیت اینکار، فراهم شدن امکانات بومی سازی، برای مثال در کتابخانه‌هایی مانند Fluent Validation است که دقیقا از سرویس غیرجنریک IStringLocalizer برای دریافت منابع استفاده می‌کنند.
          - در آخر تعریف یک سرویس سفارشی را نیز مشاهده می‌کنید که در ادامه‌ی بحث تکمیل خواهد شد.

          هدف از متد AddCustomLocalization فوق، خلوت کردن فایل startup برنامه است. این متد به صورت زیر مورد استفاده قرار می‌گیرد:
              public class Startup
              {
                  public void ConfigureServices(IServiceCollection services)
                  {
                      services.AddHttpContextAccessor();
                      services.AddControllers().AddCustomLocalization(services);
                  }

          پس از آن نیاز است میان‌افزار بومی سازی را نیز فعال کرد. متد UseCustomRequestLocalization زیر، اینکار را انجام می‌دهد:
              public static class LocalizationConfig
              {
                  public static IApplicationBuilder UseCustomRequestLocalization(this IApplicationBuilder app)
                  {
                      var requestLocalizationOptions = new RequestLocalizationOptions
                      {
                          DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")),
                          SupportedCultures = new[]
                          {
                              new CultureInfo("en-US"),
                              new CultureInfo("fa-IR")
                          },
                          SupportedUICultures = new[]
                          {
                              new CultureInfo("en-US"),
                              new CultureInfo("fa-IR")
                          }
                      };
                      app.UseRequestLocalization(requestLocalizationOptions);
                      return app;
                  }
              }
          محل قرارگیری متد UseCustomRequestLocalization فوق در فایل آغازین برنامه، باید به صورت زیر باید باشد:
              public class Startup
              {
                  public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
                  {
                      if (env.IsDevelopment())
                      {
                          app.UseDeveloperExceptionPage();
                      }
          
                      app.UseHttpsRedirection();
          
                      app.UseCustomRequestLocalization();
          
                      app.UseRouting();
          
                      app.UseAuthorization();
          
                      app.UseEndpoints(endpoints =>
                      {
                          endpoints.MapControllers();
                      });
                  }
              }


          تعریف مدل برنامه به همراه ویژگی‌های بومی سازی شده

          در اینجا تعریف RegisterModel را مشاهده می‌کنید که ErrorMessage‌های آن هرچند به ظاهر یک رشته‌ی معمولی هستند، اما در عمل از فایل منبع اشتراکی خوانده می‌شوند:
          using System.ComponentModel.DataAnnotations;
          
          namespace Core3xSharedResource.Models.Account
          {
              public class RegisterModel
              {
                  [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources
                  [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources
                  public string Email { get; set; }
              }
          }

          فایل resx ما دارای یک چنین کلیدهایی است:
          <?xml version="1.0" encoding="utf-8"?>
          <root>
            <data name="&lt;b&gt;Hello&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;" xml:space="preserve">
              <value>&lt;b&gt;سلام&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;</value>
            </data>
            <data name="About Title" xml:space="preserve">
              <value>درباره</value>
            </data>
            <data name="DNT" xml:space="preserve">
              <value>.NET Tips</value>
            </data>
            <data name="SiteName" xml:space="preserve">
              <value>DNT</value>
            </data>
            <data name="Please enter an email address" xml:space="preserve">
              <value>لطفا ایمیلی را وارد کنید</value>
            </data>
            <data name="Please enter a valid email address" xml:space="preserve">
              <value>لطفا ایمیل معتبری را وارد کنید</value>
            </data>
          </root>
          یک نکته: در اینجا بهتر است کلیدها را به صورت جملات کامل انگلیسی وارد کرد، تا اگر منبع فارسی معادل آن‌ها یافت نشدند، دقیقا از همان کلید، به عنوان مقدار خروجی سیستم بومی سازی استفاده کند.


          آزمایش برنامه

          اکنون برنامه‌ی Web API، ‌برای آزمایش آماده‌است. برای مثال در کنترلر زیر، سرویس عمومی IStringLocalizer به سازنده‌ی کلاس تزریق شده‌است و سپس قصد بازگشت مقدار کلید «About Title» را دارد. همچنین خطاهای بومی شده‌ی مدل برنامه را نیز بررسی می‌کنیم:
          using Core3xSharedResource.Models.Account;
          using Microsoft.AspNetCore.Mvc;
          using Microsoft.Extensions.Localization;
          
          namespace Core3xSharedResource.WebApi.Controllers
          {
              [ApiController]
              [Route("[controller]")]
              public class NormalIStringLocalizerController : ControllerBase
              {
                  private readonly IStringLocalizer _localizer;
          
                  public NormalIStringLocalizerController(IStringLocalizer localizer)
                  {
                      _localizer = localizer;
                  }
          
                  [HttpGet]
                  public ActionResult<string> Get()
                  {
                      var localizedString = _localizer["About Title"];
                      if (localizedString.ResourceNotFound)
                      {
                          return NotFound($"The localization resource with ID:`{localizedString.Name}` not found. SearchedLocation: `{localizedString.SearchedLocation}`.");
                      }
                      return localizedString.Value;
                  }
          
                  [HttpPost]
                  public ActionResult<RegisterModel> Post(RegisterModel model)
                  {
                      return model;
                  }
              }
          }


          حالت get را در تصویر فوق مشاهده می‌کنید. در Web API برای تنظیم زبان مورد استفاده می‌توان از هدری به نام Accept-Language استفاده کرد که برای مثال در اینجا به fa تنظیم شده‌است و نتیجه‌ی آن مراجعه به فایل SharedResource.fa.resx خواهد بود. اگر en-us وارد شود، نیاز خواهد بود تا فایل منبع اشتراکی دیگری را تعریف کنید. البته اگر این هدر تنظیم نشود، با توجه به تنظیمات متد UseCustomRequestLocalization، مقدار پیش‌فرض آن همان fa-IR خواهد بود.

          حالت post را نیز در تصویر زیر می‌توان مشاهده کرد:


          در اینجا چون ایمیل وارد نشده، هر دو خطای تنظیم شده‌ی در مدل برنامه را دریافت کرده‌ایم و این خطاها نیز فارسی هستند. به این معنا که بومی سازی data annotations نیز به درستی کار می‌کند.


          تعریف یک سرویس عمومی برای محصور سازی قابلیت‌های بومی سازی، در برنامه‌های Web API

          در ادامه تعریف سرویس SharedResourceService را مشاهده می‌کنید که ثبت آن‌را پیشتر انجام دادیم:
          using System;
          using System.Collections.Generic;
          using Microsoft.Extensions.Localization;
          using Microsoft.Extensions.Logging;
          using Microsoft.AspNetCore.Http;
          
          namespace Core3xSharedResource.Services
          {
              public interface ISharedResourceService
              {
                  string this[string index] { get; }
          
                  IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
                  string GetString(string name, params object[] arguments);
                  string GetString(string name);
              }
          
              public class SharedResourceService : ISharedResourceService
              {
                  private readonly IStringLocalizer _sharedLocalizer;
                  private readonly ILogger<SharedResourceService> _logger;
                  private readonly IHttpContextAccessor _httpContextAccessor;
          
                  public SharedResourceService(
                      IStringLocalizer sharedHtmlLocalizer,
                      IHttpContextAccessor httpContextAccessor,
                      ILogger<SharedResourceService> logger
                      )
                  {
                      _logger = logger ?? throw new ArgumentNullException(nameof(logger));
                      _sharedLocalizer = sharedHtmlLocalizer ?? throw new ArgumentNullException(nameof(sharedHtmlLocalizer));
                      _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
                  }
          
                  public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
                  {
                      return _sharedLocalizer.GetAllStrings(includeParentCultures);
                  }
          
                  public string this[string index] => GetString(index);
          
                  public string GetString(string name, params object[] arguments)
                  {
                      var result = _sharedLocalizer.GetString(name, arguments);
                      logError(name, result);
                      return result;
                  }
          
                  private void logError(string name, LocalizedString result)
                  {
                      if (result.ResourceNotFound)
                      {
                          var acceptLanguage = _httpContextAccessor?.HttpContext?.Request?.Headers["Accept-Language"];
                          _logger.LogError($"The localization resource with Accept-Language:`{acceptLanguage}` & ID:`{name}` not found. SearchedLocation: `{result.SearchedLocation}`.");
                      }
                  }
          
                  public string GetString(string name)
                  {
                      var result = _sharedLocalizer.GetString(name);
                      logError(name, result);
                      return result;
                  }
              }
          }
          این سرویس نه فقط دسترسی به IStringLocalizer را محصور می‌کند، بلکه در متد logError آن اینبار خطای بسیار مفیدی جهت دیباگ کردن سیستم بومی سازی لاگ خواهد شد. اگر کلیدی یافت نشود، فایلی یافت نشود و یا زبان ارسالی تنظیمی یافت نشود، خطای آن‌را در لاگ‌های برنامه می‌توانید مشاهده کنید که در حالت عادی کار با IStringLocalizer، لاگ نمی‌شوند و همچنین هیچ خطا و یا استثنائی را نیز سبب نمی‌شوند. به همین جهت دیباگ کردن سیستم بومی سازی بدون این لاگ‌ها، تقریبا غیرممکن است. برای مثال مقدار baseNameهایی را که در کدهای این مطلب مشاهده می‌کنید، بر اساس همین لاگ‌ها تشخیص داده شدند و بدون آن‌ها تشکیل این مقادیر غیرممکن بودند.


          کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Core3xSharedResource.zip
          مطالب
          Globalization در ASP.NET MVC - قسمت ششم
          در قسمت قبل ساختار اصلی و پیاده‌سازی ابتدایی یک پرووایدر سفارشی دیتابیسی شرح داده شد. در این قسمت ادامه بحث و مطالب پیشرفته‌تر آورده شده است.

          تولید یک پرووایدر منابع دیتابیسی - بخش دوم
          در بخش دوم این سری مطلب، ساختار دیتابیس و مباحث پیشرفته پیاده‌سازی کلاس‌های نشان داده‌شده در بخش اول در قسمت قبل شرح داده می‌شود. این مباحث شامل نحوه کش صحیح و بهینه داده‌های دریافتی از دیتابیس، پیاده‌سازی فرایند fallback، و پیاده‌سازی مناسب کلاس DbResourceManager برای مدیریت کل عملیات است.
           
          ساختار دیتابیس
          برای پیاده‌سازی منابع دیتابیسی روش‌های مختلفی برای آرایش جداول جهت ذخیره انواع ورودی‌ها می‌توان درنظر گرفت.
          مثلا درصورتی‌که حجم و تعداد منابع بسیار باشد و نیز منابع دیتابیسی به اندازه کافی در دسترس باشد، می‌توان به ازای هر منبع یک جدول درنظر گرفت.
          یا درصورتیکه منابع داده‌ای محدودتر باشند می‌توان به ازای هر کالچر یک جدول درنظر گرفت و تمام منابع مربوط به یک کالچر را درون یک جدول ذخیره کرد. درهرصورت نحوه انتخاب آرایش جداول منابع کاملا بستگی به شرایط کاری و سلایق برنامه‌نویسی دارد.
          برای مطلب جاری به عنوان یک راه‌حل ساده و کارآمد برای پروژه‌های کوچک و متوسط، تمام ورودی‌های منابع درون یک جدول با ساختاری مانند زیر ذخیره می‌شود:


          نام این جدول را با درنظر گرفتن شرایط موجود می‌توان Resources گذاشت.

          ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.

          ستون Key برای نگهداری کلید ورودی منبع استفاده می‌شود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره می‌شود. 

          ستون Culture برای ذخیره کالچر ورودی منبع به کار می‌رود. این مقدار می‌تواند برای کالچر پیش‌فرض برنامه برابر رشته خالی باشد. 

          ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده می‌شود. 

          برای ستون Id می‌توان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.

          نکته: برای امنیت بیشتر می‌توان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.

          برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش می‌دهد دقت کنید:

           

          اصلاح کلاس DbResourceProviderFactory

          برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید به‌صورت زیر تغییر کند:

          public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
          {
            if (!string.IsNullOrEmpty(virtualPath))
            {
              virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/'
            }
            return new LocalDbResourceProvider(virtualPath);
          }
          با این تغییر مسیرهای درخواستی چون "Default.aspx/~" و یا "Default.aspx/" هر دو به صورت "Default.aspx" در می‌آیند تا با نام ذخیره شده در دیتابیس یکسان شوند.
           

          ارتباط با دیتابیس

          خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راه‌های زیادی وجود دارد. برای پیاده‌سازی آن مثلا می‌توان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا به‌کارگیری IoC، نمونه مناسبی از پیاده‌سازی اینترفیس مذبور را در اختیار برنامه قرار داد.
          اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیاده‌سازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.

          پس از پیاده‌سازی کلاس‌های مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با داده‌ها استفاده می‌شود که نمونه‌ای ابتدایی از آن در زیر آورده شده است:

          using System.Collections.Generic;
          using System.Linq;
          using DbResourceProvider.Models;
          
          namespace DbResourceProvider.Data
          {
            public class ResourceData
            {
              private readonly string _resourceName;
              public ResourceData(string resourceName)
              {
                _resourceName = resourceName;
              }
              public Resource GetResource(string resourceKey, string culture)
              {
                using (var data = new TestContext())
                {
                  return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture);
                }
              }
              public List<Resource> GetResources(string culture)
              {
                using (var data = new TestContext())
                {
                  return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList();
                }
              }
            }
          }
          کلاس فوق نسبت به نمونه‌ای که در قسمت قبل نشان داده شد کمی فرق دارد. بدین صورت که برای راحتی بیشتر نام منبع درخواستی به جای پارامتر متدها، در اینجا به عنوان پارامتر کانستراکتور وارد می‌شود.

          نکته: درصورتی‌که این کلاس‌ها در پروژه‌ای جداگانه قرار دارند، باید ConnectionString مربوطه در فایل کانفیگ برنامه مقصد نیز تنظیم شود.

          کش کردن ورودی‌ها
          برای کش کردن ورودی‌ها این نکته را که قبلا هم به آن اشاره شده بود باید درنظر داشت:
          پس از اولین درخواست برای هر منبع، نمونه تولیدشده از پرووایدر مربوطه در حافظه سرور کش خواهد شد.
          یعنی متدهای کلاس DbResourceProviderFactory به‌ازای هر منبع تنها یکبار فراخوانی می‌شود. نمونه‌های کش‌شده از پروایدرهای کلی و محلی به همراه تمام محتویاتشان (مثلا نمونه تولیدی از کلاس DbResourceManager) تا زمان Unload شدن سایت در حافظه سرور باقی می‌مانند. بنابراین عملیات کشینگ ورودی‌ها را می‌توان درون خود کلاس DbResourceManager به ازای هر منبع انجام داد.
          برای کش کردن ورودی‌های هر منبع می‌توان چند روش را درپیش گرفت. روش اول این است که به ازای هر کلید درخواستی تنها ورودی مربوطه از دیتابیس فراخوانی شده و در برنامه کش شود. این روش برای حالاتی که تعداد ورودی‌ها یا تعداد درخواست‌های کلیدهای هر منبع کم باشد مناسب خواهد بود.
          یکی از پیاده‌سازی این روش این است که ورودی‌ها به ازای هر کالچر ذخیره شوند. پیاده‌سازی اولیه این نوع فرایند کشینگ در کلاس DbResourceManager به صورت زیر است:
          using System.Collections.Generic;
          using System.Globalization;
          using DbResourceProvider.Data;
          namespace DbResourceProvider
          {
            public class DbResourceManager
            {
              private readonly string _resourceName;
              private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture;
              public DbResourceManager(string resourceName)
              {
                _resourceName = resourceName;
                _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>();
              }
              public object GetObject(string resourceKey, CultureInfo culture)
              {
                return GetCachedObject(resourceKey, culture.Name);
              }
              private object GetCachedObject(string resourceKey, string cultureName)
              {
                if (!_resourceCacheByCulture.ContainsKey(cultureName))
                  _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>());
                var cachedResource = _resourceCacheByCulture[cultureName];
                lock (this)
                {
                  if (!cachedResource.ContainsKey(resourceKey))
                  {
                    var data = new ResourceData(_resourceName);
                    var dbResource = data.GetResource(resourceKey, cultureName);
                    if (dbResource == null) return null;
                    var cachedResources = _resourceCacheByCulture[cultureName];
                    cachedResources.Add(dbResource.Key, dbResource.Value);
                  }
                }
                return cachedResource[resourceKey];
              }
            }
          }
          همانطور که قبلا توضیح داده شد کشِ پرووایدرهای منابع به ازای هر منبع درخواستی (و به تبع آن نمونه‌های موجود در آن مثل DbResourceManager) برعهده خود ASP.NET است. بنابراین برای کش کردن ورودی‌های درخواستی هر منبع در کلاس DbResourceManager تنها کافی است آن‌ها را درون یک متغیر محلی در سطح کلاس (فیلد) ذخیره کرد. کاری که در کد بالا در متغیر resourceCacheByCulture_ انجام شده است. در این متغیر که از نوع دیکشنری تعریف شده است کلیدهای هر عضو آن برابر نام کالچر مربوطه است. مقادیر هر عضو این دیکشنری نیز خود یک دیکشنری است که ورودی‌های منابع مربوط به کالچر مربوطه در آن ذخیره می‌شوند.
          عملیات در متد GetCachedObject انجام می‌شود. همان‌طور که می‌بینید ابتدا وجود ورودی موردنظر در متغیر کشینگ بررسی می‌شود و درصورت عدم وجود، مقدار آن مستقیما از دیتابیس درخواست می‌شود. سپس این مقدار درخواستی ابتدا درون متغیر کشینگ ذخیره شده (به همراه بلاک lock) و درنهایت برگشت داده می‌شود.

          نکته: کل فرایند بررسی وجود کلید در متغیر کشینگ (شرط دوم در متد GetCachedObject) درون بلاک lock قرار داده شده است تا در درخواست‌های همزمان احتمال افزودن چندباره یک کلید ازبین برود.

          پیاده‌سازی دیگر این فرایند کشینگ، ذخیره ورودی‌ها براساس نام کلید به جای نام کالچر است. یعنی کلید دیکشنری اصلی نام کلید و کلید دیکشنری داخلی نام کالچر است که این روش زیاد جالب نیست.
          روش دوم که بیشتر برای برنامه‌های بزرگ با ورودی‌ها و درخواست‌های زیاد به‌کار می‌رود این است که درهر بار درخواست به دیتابیس به جای دریافت تنها همان ورودی درخواستی، تمام ورودی‌های منبع و کالچر درخواستی استخراج شده و کش می‌شود تا تعداد درخواست‌های به سمت دیتابیس کاهش یابد. برای پیاده‌سازی این روش کافی است تغییرات زیر در متد GetCachedObject اعمال شود:
          private object GetCachedObject(string resourceKey, string cultureName)
          {
            lock (this)
            {
              if (!_resourceCacheByCulture.ContainsKey(cultureName))
              {
                _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>());
                var cachedResources = _resourceCacheByCulture[cultureName];
                var data = new ResourceData(_resourceName);
                var dbResources = data.GetResources(cultureName);
                foreach (var dbResource in dbResources)
                {
                  cachedResources.Add(dbResource.Key, dbResource.Value);
                }
              }
            }
            var cachedResource = _resourceCacheByCulture[cultureName];
            return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey];
          }
          دراینجا هم می‌توان به جای استفاده از نام کالچر برای کلید دیکشنری اصلی از نام کلید ورودی منبع استفاده کرد که چندان توصیه نمی‌شود.

          نکته: انتخاب یکی از دو روش فوق برای فرایند کشینگ کاملا به شرایط موجود و سلیقه برنامه نویس بستگی دارد.

          فرایند Fallback
          درباره فرایند fallback به اندازه کافی در قسمت‌های قبلی توضیح داده شده است. برای پیاده‌سازی این فرایند ابتدا باید به نوعی به سلسله مراتب کالچرهای موجود از کالچر جاری تا کالچر اصلی و پیش فرض سیستم دسترسی پیدا کرد. برای اینکار ابتدا باید با استفاده از روشی کالچر والد یک کالچر را بدست آورد. کالچر والد کالچری است که عمومیت بیشتری نسبت به کالچر موردنظر دارد. مثلا کالچر fa، کالچر والد fa-IR است. همچنین کالچر Invariant به عنوان والد تمام کالچرها شناخته می‌شود.
          خوشبختانه در کلاس CultureInfo (که در قسمت‌های قبلی شرح داده شده است) یک پراپرتی با عنوان Parent وجود دارد که کالچر والد را برمی‌گرداند.
          برای رسیدن به سلسله مراتب مذبور در کلاس ResourceManager دات نت، از کلاسی با عنوان ResourceFallbackManager استفاده می‌شود. هرچند این کلاس با سطح دسترسی internal تعریف شده است اما نام‌گذاری نامناسبی دارد زیرا کاری که می‌کند به عنوان Manager هیچ ربطی ندارد. این کلاس با استفاده از یک کالچر ورودی، یک enumerator از سلسله مراتب کالچرها که در بالا صحبت شد تهیه می‌کند.
          با استفاده پیاده‌سازی موجود در کلاس ResourceFallbackManager کلاسی با عنوان CultureFallbackProvider تهیه کردم که به صورت زیر است:
          using System.Collections;
          using System.Collections.Generic;
          using System.Globalization;
          namespace DbResourceProvider
          {
            public class CultureFallbackProvider : IEnumerable<CultureInfo>
            {
              private readonly CultureInfo _startingCulture;
              private readonly CultureInfo _neutralCulture;
              private readonly bool _tryParentCulture;
              public CultureFallbackProvider(CultureInfo startingCulture = null, 
                                             CultureInfo neutralCulture = null, 
                                             bool tryParentCulture = true)
              {
                _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture;
                _neutralCulture = neutralCulture;
                _tryParentCulture = tryParentCulture;
              }
              #region Implementation of IEnumerable<CultureInfo>
              public IEnumerator<CultureInfo> GetEnumerator()
              {
                var reachedNeutralCulture = false;
                var currentCulture = _startingCulture;
                do
                {
                  if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name)
                  {
                    yield return CultureInfo.InvariantCulture;
                    reachedNeutralCulture = true;
                    break;
                  }
                  yield return currentCulture;
                  currentCulture = currentCulture.Parent;
                } while (_tryParentCulture && !HasInvariantCultureName(currentCulture));
                if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture)
                  yield break;
                yield return CultureInfo.InvariantCulture;
              }
              #endregion
              #region Implementation of IEnumerable
              IEnumerator IEnumerable.GetEnumerator()
              {
                return GetEnumerator();
              }
              #endregion
              private bool HasInvariantCultureName(CultureInfo culture)
              {
                return culture.Name == CultureInfo.InvariantCulture.Name;
              }
            }
          }
          این کلاس که اینترفیس <IEnumerable<CultureInfo را پیاده‎سازی کرده است، سه پارامتر کانستراکتور دارد.
          اولین پارامتر، کالچر جاری یا آغازین را مشخص می‌کند. این کالچری است که تولید enumerator مربوطه از آن آغاز می‌شود. درصورتی‌که این پارامتر نال باشد مقدار کالچر UI در ثرد جاری برای آن درنظر گرفته می‌شود. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، null است.
          پارامتر بعدی کالچر خنثی موردنظر کاربر است. این کالچری است که درصورت رسیدن enumerator به آن کار پایان خواهد یافت. درواقع کالچر پایانی enumerator است. این پارامتر می‌تواند نال باشد. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، null است.
          پارمتر آخر هم تعیین می‌کند که آیا enumerator از کالچرهای والد استفاده بکند یا خیر. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، true است. 
          کار اصلی کلاس فوق در متد GetEnumerator انجام می‌شود. در این کلاس یک حلقه do-while وجود دارد که enumerator را با استفاده از کلمه کلیدی yield تولید می‌کند. در این متد ابتدا درصورت نال نبودن کالچر خنثی ورودی، بررسی می‌شود که آیا نام کالچر جاری حلقه (که در متغیر محلی currentCulture ذخیره شده است) برابر نام کالچر خنثی است یا خیر. درصورت برقراری شرط، کار این حلقه با برگشت CultureInfo.InvariantCulture پایان می‌‌یابد. InvariantCulture کالچر بدون زبان و فرهنگ و موقعیت مکانی است که درواقع به عنوان کالچر والد تمام کالچرها درنظر گرفته می‌شود. پراپرتی Name این کالچر برابر string.Empty است.
          کار حلقه با برگشت مقدار کالچر جاری enumerator ادامه می‌یابد. سپس کالچر جاری با کالچر والدش مقداردهی می‌شود. شرط قسمت while حلقه تعیین می‌کند که درصورتی‌که کلاس برای استفاده از کالچرهای والد تنظیم شده باشد، تا زمانی که نام کالچر جاری برابر نام کالچر Invariant نباشد، تولید اعضای enumerator ادامه یابد.
          درانتها نیز درصورتی‌که با شرایط موجود، قبلا کالچر Invariant برگشت داده نشده باشد این کالچر نیز yield می‌شود. درواقع درصورتی‌که استفاده از کالچرهای والد اجازه داده نشده باشد یا کالچر آغازین برابر کالچر Invariant باشد و یا قبلا به دلیل رسیدن به کالچر خنثی ورودی، مقدار کالچر Invariant برگشت داده شده باشد، enumerator قطع شده و عملیات پایان می‌یابد. در غیر اینصورت کالچر Invariant به عنوان کالچر پایانی برگشت داده می‌شود.
           
          استفاده از CultureFallbackProvider
          با استفاده از کلاس CultureFallbackProvider می‌توان عملیات جستجوی ورودی‌های درخواستی را با ترتیبی مناسب بین تمام کالچرهای موجود به انجام رسانید.
          برای استفاده از این کلاس باید تغییراتی در متد GetObject کلاس DbResourceManager به صورت زیر اعمال کرد:
          public object GetObject(string resourceKey, CultureInfo culture)
          {
            foreach (var currentCulture in new CultureFallbackProvider(culture))
            {
              var value = GetCachedObject(resourceKey, currentCulture.Name);
              if (value != null) return value;
            }
            throw new KeyNotFoundException("The specified 'resourceKey' not found.");
          }
          با استفاده از یک حلقه foreach درون enumerator کلاس CultureFallbackProvider، کالچرهای موردنیاز برای fallback یافته می‌شوند. در اینجا از مقادیر پیش‌فرض دو پارامتر دیگر کانستراکتور کلاس CultureFallbackProvider استفاده شده است.
          سپس به ازای هر کالچر یافته شده مقدار ورودی درخواستی بدست آمده و درصورتی‌که نال نباشد (یعنی ورودی موردنظر برای کالچر جاری یافته شود) آن مقدار برگشت داده می‌شود و درصورتی‌که نال باشد عملیات برای کالچر بعدی ادامه می‌یابد.
          درصورتی‌که ورودی درخواستی یافته نشود (خروج از حلقه بدون برگشت مقداری برای ورودی منبع درخواستی) استثنای KeyNotFoundException صادر می‌شود تا کاربر را از اشتباه رخداده مطلع سازد.

          آزمایش پرووایدر سفارشی

          ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.

          داده‌های نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:

          <asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" />
          <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
          خروجی برای کالچر "en-US" (معمولا پیش‌فرض، اگر تنظیمات سیستم عامل تغییر نکرده باشد) چیزی شبیه تصویر زیر خواهد بود:

          سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):

          <globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
          بنابراین صفحه Default.aspx با همان داده‌های نشان داده شده در بالا به صورت زیر تغییر خواهد کرد:

          می‌بینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.

          بحث و نتیجه‎‌گیری

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

          اما نکته‌ای را که باید به آن توجه کنید این است که در پیاده‌سازی‌های نشان داده شده با توجه به نحوه کش‌شدن مقادیر ورودی‌ها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونه‌های تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از به‌روزرسانی ورودی‌های کش‌شده اطلاع یابد تا آنها را ریفرش کند.

          در ادامه درباره روش‌های مختلف نحوه پیاده‌سازی قابلیت به‌روزرسانی ورودی‌های منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راه‌حل‌های مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژه‌های MVC شرح داده می‌شود.

          البته مباحث پیشرفته‌تری چون تزریق وابستگی برای پیاده‌سازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.

          منابع

          http://weblogs.asp.net/thangchung/archive/2010/06/25/extending-resource-provider-for-soring-resources-in-the-database.aspx

          http://msdn.microsoft.com/en-us/library/aa905797.aspx

          http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx

          http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs

          http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider

          http://www.west-wind.com/presentations/wwdbresourceprovider

          مطالب
          پلاگین DataTables کتابخانه jQuery - قسمت چهارم
          همان طور که قبلا اشاره کردیم، این پلاگین می‌تواند از یک زبان برنامه نویسی سمت سرور داده‌های مورد نیاز خودش را دریافت کند. می‌توانید داده‌ها را با استفاده از AJAX و به صورت JSON از سرور دریافت کرده و با استفاده از DataTables آنها را در جدول تزریق کنید. در این قسمت سعی خواهیم کرد تا با استفاده از jQuery DataTables یک گرید را در MVC ایجاد کنیم.  البته برای حذف جزئیات داده‌ها به جای این که از یک بانک اطلاعاتی دریافت شوند، در حافظه ساخته می‌شوند. در هر صورت اساس کار یکی است.

          قصد داریم تا مانند مثال قسمت قبل، مجموعه ای از اطلاعات مربوط به مرورگرهای مختلف را در یک جدول نشان دهیم، اما این بار منبع داده ما فرق می‌کند. منبع داده از طرف سرور فراهم می‌شود. هر مرورگر - همان طور که در قسمت قبل مشاهده نمودید - شامل اطلاعات زیر خواهد بود:
          1. موتور رندرگیری (Engine)
          2. نام مرورگر (Name)
          3. پلتفرم (Platform)
          4. نسخه موتور (Version)
          5. نمره سی اس اس (Grade)

          به همین دلیل در سمت سرور، کلاسی خواهیم ساخت که نمایانگر یک مرورگر باشد. بدین صورت:

          public class Browser
          {
              public int Id { get; set; }
              public string Engine { get; set; }
              public string Name { get; set; }
              public string Platform { get; set; }
              public float Version { get; set; }
              public string Grade { get; set; }
          }

          استفاده از روش server side processing برای دریافت داده‌ها از سرور

          این روش، یکی از امکانات jQuery DataTables است که با استفاده از آن، کلاینت تنها یک مصرف کننده صرف خواهد بود و وظیفه پردازش اطلاعات - یعنی تعداد رکوردهایی که برگشت داده می‌شود، صفحه بندی، مرتب سازی، جستجو، و غیره - به عهده سرور خواهد بود.

          برای به کار گیری این روش، اولین کار این است که ویژگی bServerSide را true کنیم، مثلا بدین صورت:
          var $table = $('#browsers-grid');
          $table.dataTable({
                "bServerSide": true,
                "sAjaxSource": "/Home/GetBrowsers"
           });

          همچنین ویژگی sAjaxSource را به Url ی که باید داده‌ها از آن دریافت شوند مقداردهی می‌کنیم.

          به صورت پیش فرض مقدار ویژگی bServerSide مقدار false است؛ که یعنی منبع داده این پلاگین از سمت سرور خوانده نشود. اگر true باشد منبع داده و خیلی اطلاعات دیگر مربوط به داده‌های درون جدول باید از سرور به مرورگر کاربر پس فرستاده شوند. با true کردن مقدار bServerSide، آنگاه DataTables اطلاعاتی را راجع به شماره صفحه جاری، اندازه هر صفحه، شروط فیلتر کردن داده ها، مرتب سازی ستون ها، و غیره را به سرور می‌فرستد. همجنین انتظار می‌رود تا سرور در پاسخ به این درخواست، داده‌های مناسبی را به فرمت JSON به مرورگر پس بفرستد. در حالتی که bServerSide مقدار true به خود بگیرد، پلاگین فقط رابطه متقابل بین کاربر و سرور را مدیریت می‌کند و هیچ پردازشی را انجام نمی‌دهد.


          در این درخواست XHR یا Ajax ی پارامترهایی که به سرور ارسال می‌شوند این‌ها هستند:

          iDisplayStart عدد صحیح
          نقظه شروع مجموعه داده جاری

          iDisplayLength عدد صحیح
          تعداد رکوردهایی که جدول می‌تواند نمایش دهد. تعداد رکوردهایی که از طرف سرور برگشت داده می‌شود باید با این عدد یکسان باشند.

          iColumns عدد صحیح
          تعداد ستونهایی که باید نمایش داده شوند.

          sSearch رشته
          فیلد جستجوی عمومی

          bRegex بولین
          اگر true باشد معنی آن این است که می‌توان از عبارات باقاعده برای جستجوی عبارتی خاص در کل ستون‌های جدول استفاده کرد. مثلا در کادر جستجو نوشت :

          ^[1-5]$
          که یعنی 1 و 5 همه عددهای بین 1و 5.

          bSearchable_(int)    بولین
          نمایش می‌دهد که یک ستون در طرف کاربر قابلیت searchable آن true هست یا نه.

          sSearch_(int)   رشته
          فیلتر مخصوص هر ستون. اگر از ویژگی multi column filtering پلاگین استفاده شود به صورت sSearch0 ، sSearch1 ، sSeach2 و ... به طرف سرور ارسال می‌شوند. شماره انتهای هر کدام از پارامترها بیانگر شماره ستون جدول است.

          bRegex_(int)  بولین
          اگر true باشد، بیان می‌کند که می‌توان از عبارت با قاعده در ستون شماره int جهت جستجو استفاده کرد.

          bSortable_(int) بولین
          مشخص می‌کند که آیا یک ستون در سمت کلاینت، قابلیت مرتب شدن بر اساس آن وجود دارد یا نه. (در اینجا int اندیس ستون را مشخص می‌کند)

          iSortingCols   عدد صحیح
          تعداد ستون هایی که باید مرتب سازی بر اساس آنها صورت پذیرد. در صورتی که از امکان multi column sorting استفاده کنید این مقدار می‌تواند بیش از یکی باشد.

          iSortCol_(int)   عدد صحیح
          شماره ستونی که باید بر اساس آن عملیات مرتب سازی صورت پذیرد.

          sSortDir_(int)    رشته
          نحوه مرتب سازی ؛ شامل صعودی (asc) یا نزولی (desc)

          mDataProp_(int)    رشته
          اسم ستون‌های درون جدول را مشخص می‌کند.

          sEcho     رشته
          اطلاعاتی که datatables از آن برای رندر کردن جدول استفاده می‌کند.

          شکل زیر نشان می‌دهد که چه پارامترهایی به سرور ارسال می‌شوند.



          شکل ب ) پارامترهای ارسالی به سرور به صورت json

          بعضی از این پارامترها بسته به تعداد ستون‌ها قابل تغییر هستند. (آن پارامترهایی که آخرشان یک عدد هست که نشان دهنده شماره ستون مورد نظر می‌باشد)

          در پاسخ به هر درخواست XHR که datatables به سرور می‌فرستد، انتظار دارد تا سرور نیز یک شیء json را با فرمت مخصوص که شامل پارامترهای زیر می‌شود به او پس بفرستد:

          iTotalRecords    عدد صحیح
          تعداد کل رکوردها (قبل از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهای درون آن جدول از دیتابیس که داده‌ها باید از آن دریافت شوند. تعداد کل رکوردهایی که در طرف سرور وجود دارند. این مقدار فقط برای نمایش به کاربر برگشت داده می‌شود و نیز از آن برای صفحه بندی هم استفاده می‌شود. 


          iTotalDisplayRecords    عدد صحیح
          تعداد کل رکوردها (بعد از عملیات جستجو) یا به عبارت دیگر تعداد کل رکوردهایی که بعد از عملیات جستجو پیدا می‌شوند نه فقط آن تعداد رکوردی که به کاربر پس فرستاده می‌شوند. تعداد کل رکوردهایی که با شرط جستجو مطابقت دارند. اگر کاربر چیزی را جستجو نکرده باشد مقدار این پارامتر با پارامتر iTotalRecords یکسان خواهد بود.  

          sEcho    عدد صحیح 
          یک عدد صحیح است که در قالب رشته در تعامل بین سرور و کلاینت جا به جا می‌شود. این مقدار به ازاء هر درخواست تغییر می‌کند. همان مقداری که مرورگر به سرور می‌دهد را سرور هم باید به مرورگر تحویل بدهد. برای جلوگیری از حملات XSS باید آن را تبدیل به عدد صحیح کرد. پلاگین DataTables مقدار این پارامتر را برای هماهنگ کردن و منطبق کردن درخواست ارسال شده و جواب این درخواست استفاده می‌کند. همان مقداری که مروگر به سرور می‌دهد را باید سرور تحویل به مرورگر بدهد. 

          sColumns    رشته
          اسم ستون‌ها که با استفاده از کاما از هم جدا شده اند. استفاده از آن اختیاری است و البته منسوخ هم شده است و در نسخه‌های جدید jQuery DataTables از آن پشتیبانی نمی‌شود.

          aaData    آرایه
          همان طور که قبلا هم گفتیم، مقادیر سلول هایی را که باید در جدول نشان داده شوند را در خود نگهداری می‌کند. یعنی در واقع داده‌های جدول در آن ریخته می‌شوند. هر وقت که DataTables داده‌های مورد نیازش را دریافت می‌کند، سلول‌های جدول html مربوطه اش را از روی آرایه aaData ایجاد می‌کند. تعداد ستون‌ها در این آرایه دو بعدی، باید با تعداد ستون‌های جدول html مربوطه به آن یکسان باشد

          شکل زیر پارامترها دریافتی از سرور را نشان می‌دهند:


          شکل ب ) پارامترهای دریافتی از سرور به صورت json

          استفاده از روش server side processing در mvc
          همان طور که گفتیم، کلاینت به سرور یک سری پارامترها را ارسال می‌کند و آن پارامترها را هم شرح دادیم. برای دریافت این پارامتر‌ها طرف سرور، احتیاج به یک مدل هست. این مدل به صورت زیر پیاده سازی خواهد شد:
          /// <summary>
          /// Class that encapsulates most common parameters sent by DataTables plugin
          /// </summary>
          public class jQueryDataTableParamModel
          {
              /// <summary>
              /// Request sequence number sent by DataTable,
              /// same value must be returned in response
              /// </summary>
              public string sEcho { get; set; }
              /// <summary>
              /// Text used for filtering
              /// </summary>
              public string sSearch { get; set; }
              /// <summary>
              /// Number of records that should be shown in table
              /// </summary>
              public int iDisplayLength { get; set; }
              /// <summary>
              /// First record that should be shown(used for paging)
              /// </summary>
              public int iDisplayStart { get; set; }
              /// <summary>
              /// Number of columns in table
              /// </summary>
              public int iColumns { get; set; }
              /// <summary>
              /// Number of columns that are used in sorting
              /// /// </summary>
              public int iSortingCols { get; set; }
              /// <summary>
              /// Comma separated list of column names
              /// </summary>
              public string sColumns { get; set; }
          }

          مدل بایندر mvc وظیفه مقداردهی به خصوصیات درون این کلاس را بر عهده دارد، بقیه پارامترهایی که به سرور ارسال می‌شوند و در این کلاس نیامده اند، از طریق شیء Request در دسترس خواهند بود.


          اکشن متدی که مدل بالا را دریافت می‌کند، می‌تواند به صورت زیر پیاده سازی شود. این اکشن متد وظیفه پاسخ دادن به درخواست DataTables بر اساس پارامترهای ارسال شده در مدل DataTablesParam را دارد. خروجی این اکشن متد شامل پارارمترهای مورد نیاز پلاگین DataTables برای تشکیل جدول است که آنها را هم شرح دادیم.

          public JsonResult GetBrowsers(jQueryDataTableParamModel param)
          {
                  IQueryable<Browser> allBrowsers = new Browsers().CreateInMemoryDataSource().AsQueryable();
          
                  IEnumerable<Browser> filteredBrowsers;
          
                  // Apply Filtering
                  if (!string.IsNullOrEmpty(param.sSearch))
                  {
                          filteredBrowsers = new Browsers().CreateInMemoryDataSource()
                              .Where(x => x.Engine.Contains(param.sSearch)
                                                 || x.Grade.Contains(param.sSearch)
                                                 || x.Name.Contains(param.sSearch)
                                                 || x.Platform.Contains(param.sSearch)
                              ).ToList();
                          float f;
                          if (float.TryParse(param.sSearch, out f))
                          {
                              filteredBrowsers = filteredBrowsers.Where(x => x.Version.Equals(f));
                          }
                  }
                  else
                  {
                          filteredBrowsers = allBrowsers;
                  }
          
                  // Apply Sorting
                  var sortColumnIndex = Convert.ToInt32(Request["iSortCol_0"]);
                  Func<Browser, string> orderingFunction = (x => sortColumnIndex == 0 ? x.Engine :
                                                                      sortColumnIndex == 1 ? x.Name :
                                                                      sortColumnIndex == 2 ? x.Platform :
                                                                      sortColumnIndex == 3 ? x.Version.ToString() :
                                                                      sortColumnIndex == 4 ? x.Grade :
                                                                          x.Name);
          
                  var sortDirection = Request["sSortDir_0"]; // asc or desc
                  filteredBrowsers = sortDirection == "asc" ? filteredBrowsers.OrderBy(orderingFunction) : filteredBrowsers.OrderByDescending(orderingFunction);
          
                  // Apply Paging
                  var enumerable = filteredBrowsers.ToArray();
                  IEnumerable<Browser> displayedBrowsers = enumerable.Skip(param.iDisplayStart).
                          Take(param.iDisplayLength).ToList();
          
                  return Json(new
                  {
                          sEcho = param.sEcho,
                          iTotalRecords = allBrowsers.Count(),
                          iTotalDisplayRecords = enumerable.Count(),
                          aaData = displayedBrowsers
                  }, JsonRequestBehavior.AllowGet);
          }

          تشریح اکشن متد GetBrowsers :

          این اکشن متد از مدل jQueryDataTableParamModel به عنوان پارامتر ورودی خود استفاده می‌کند. این مدل همان طور هم که گفتیم، شامل یک سری خصوصیت است که توسط پلاگین jQuery DataTables مقداردهی می‌شوند و همچنین مدل بایندر mvc وظیفه بایند کردن این مقادیر به خصوصیات درون این کلاس را بر عهده خواهد داشت. درون بدنه اکشن متد GetBrowsers داده‌ها بعد از اعمال عملیات فیلترینگ، مرتب سازی، و صفحه بندی به فرمت مناسبی درآمده و به طرف مرورگر فرستاده خواهند شد.

          برای پیاده سازی کدهای طرف کلاینت نیز، درون یک View کدهای زیر قرار خواهند گرفت:
          $(function () {
                  var $table = $('#browsers-grid');
                  $table.dataTable({
                          "bProcessing": true,
                          "bStateSave": true,
                          "bServerSide": true,
                          "bFilter": true,
                          "sDom": 'T<"clear">lftipr',
                          "aLengthMenu": [[5, 10, 25, 50, -1], [5, 10, 25, 50, "All"]],
                          "bAutoWidth": false,
                          "sAjaxSource": "/Home/GetBrowsers",
                          "fnServerData": function (sSource, aoData, fnCallback) {
                              $.ajax({
                                  "dataType": 'json',
                                  "type": "POST",
                                  "url": sSource,
                                  "data": aoData,
                                  "success": fnCallback
                              });
                          },
                          "aoColumns": [
                              { "mDataProp": "Engine" },
                              { "mDataProp": "Name" },
                              { "mDataProp": "Platform" },
                              { "mDataProp": "Version" },
                              { "mDataProp": "Grade" }
                          ],
                          "oLanguage": {
                                  "sUrl": "/Content/dataTables.persian.txt"
                          }
                  });
          });

          تشریح کدها:

          fnServerData :

          این متد، در واقع نحوه تعامل سرور و کلاینت را با استفاده از درخواستهای XHR مشخص خواهد کرد.

          oLanguage :

          برای فعال سازی زبان فارسی، فیلدهای مورد نیاز ترجمه شده و در یک فایل متنی قرار داده شده اند. کافی است آدرس این فایل متنی به ویژگی oLanguage اختصاص داده شوند.

          مثال این قسمت را از لینک زیر دریافت کنید:
          DataTablesTutorial04.zip

          لازم به ذکر است پوشه bin، obj، و packages جهت کاهش حجم این مثال از solution حذف شده اند. برای اجرای این مثال از اینجا کمک بگیرید.


          مطالعه بیشتر

          برای مطالعه بیشتر در مورد این پلاگین و نیز پیاده سازی آن در MVC می‌توانید به لینک زیر نیز مراجعه بفرمائید که بعضی از قسمتهای این مطلب هم از مقاله زیر استفاده کرده است:
          jQuery DataTables and ASP.NET MVC Integration - Part I



          مطالب
          استفاده از LocalDb در IIS، قسمت دوم: مالکیت وهله ها
          در قسمت قبلی این مقاله گفتیم که دو خاصیت از LocalDb هنگام استفاده از Full IIS باعث بروز خطا می‌شوند:

          • LocalDb نیاز دارد که پروفایل کاربر بارگذاری شده باشد
          • بصورت پیش فرض وهله LocalDb متعلق به یک کاربر بوده، و خصوصی است

          در قسمت قبل دیدیم چگونه باید پروفایل کاربر را بدرستی بارگذاری کنیم. در این مقاله به مالکیت وهله‌ها (instance ownership) می‌پردازیم.


          مشکل وهله خصوصی

          در پایان قسمت قبلی، اپلیکیشن وب را در این حالت رها کردیم:

          همانطور که مشاهده می‌کنید با خطای زیر مواجه هستیم:

          System.Data.SqlClient.SqlException: Cannot open database "OldFashionedDB" requested by the login. The login failed.
          Login failed for user 'IIS APPPOOL\ASP.NET v4.0'. 

          این بار پیغام خطا واضح و روشن است. LocalDb با موفقیت اجرا شده و اپلیکیشن وب هم توانسته به آن وصل شود، اما این کانکشن سپس قطع شده چرا که دسترسی به وهله جاری وجود نداشته است. اکانت ApplicationPoolIdentity (در اینجا IIS APPPOOL\ASP.NET v4.0) نتوانسته به دیتابیس LocalDb وارد شود، چرا که دیتابیس مورد نظر در رشته اتصال اپلیکیشن (OldFashionedDB) وجود ندارد. عجیب است، چرا که وصل شدن به همین دیتابیس با رشته اتصال جاری در ویژوال استودیو با موفقیت انجام می‌شود.

          همانطور که در تصویر بالا مشاهده می‌کنید از ابزار SQL Server Object Explorer استفاده شده است. این ابزار توسط SQL Server Data Tools معرفی شد و در نسخه‌های بعدی ویژوال استودیو هم وجود دارد و توسعه یافته است. چطور ممکن است ویژوال استودیو براحتی بتواند به دیتابیس وصل شود، اما اپلیکیشن وب ما با همان رشته اتصال نمی‌تواند دیتابیس را باز کند؟ در هر دو صورت رشته اتصال ما بدین شکل است:

          Data Source=(localdb)\v11.0;Initial Catalog=OldFashionedDB;Integrated Security=True

          پاسخ این است که در اینجا، دو وهله از LocalDb وجود دارد. بر خلاف وهله‌های SQL Server Express که بعنوان سرویس‌های ویندوزی اجرا می‌شوند، وهله‌های LocalDb بصورت پروسس‌های کاربری (user processes) اجرا می‌شوند. هنگامی که کاربران مختلفی سعی می‌کنند به LocalDb متصل شوند، برای هر کدام از آنها پروسس‌های مجزایی اجرا خواهد شد. هنگامی که در ویژوال استودیو به localdb)\v11.0) وصل می‌شویم، وهله ای از LocalDb ساخته شده و در حساب کاربری ویندوز جاری اجرا می‌شود. اما هنگامی که اپلیکیشن وب ما در IIS می‌خواهد به همین دیتابیس وصل شود، وهله دیگری ساخته شده و در ApplicationPoolIdentity اجرا می‌شود. گرچه ویژوال استودیو و اپلیکیشن ما هر دو از یک رشته اتصال استفاده می‌کنند، اما در عمل هر کدام به وهله‌های متفاوتی از LocalDb دسترسی پیدا خواهند کرد. پس مسلما دیتابیسی که توسط وهله ای در ویژوال استودیو ساخته شده است، برای اپلیکیشن وب ما در IIS در دسترس نخواهد بود.

          یک مقایسه خوب از این وضعیت، پوشه My Documents در ویندوز است. فرض کنید در ویژوال استودیو کدی بنویسیم که در این پوشه یک فایل جدید می‌سازد. حال اگر با حساب کاربری دیگری وارد ویندوز شویم و به پوشه My Documents برویم این فایل را نخواهیم یافت. چرا که پوشه My Documents برای هر کاربر متفاوت است. بهمین شکل، وهله‌های LocalDb برای هر کاربر متفاوت است و به پروسس‌ها و دیتابیس‌های مختلفی اشاره می‌کنند.

          به همین دلیل است که اپلیکیشن وب ما می‌تواند بدون هیچ مشکلی روی IIS Express اجرا شود و دیتابیس را باز کند. چرا که IIS Express درست مانند LocalDb یک پروسس کاربری است. IIS Express توسط ویژوال استودیو راه اندازی می‌شود و روی حساب کاربری جاری اجرا می‌گردد، پس پروسس آن با پروسس خود ویژوال استودیو یکسان خواهد بود و هر دو زیر یک اکانت کاربری اجرا خواهند شد.


          راه حل ها

          درک ماهیت مشکل جاری، راه حال‌های مختلفی را برای رفع آن بدست می‌دهد. از آنجا که هر راه حل مزایا و معایب خود را دارد، بجای معرفی یک راه حال واحد چند راهکار را بررسی می‌کنیم.


          رویکرد 1: اجرای IIS روی کاربر جاری ویندوز

          اگر مشکل، حساب‌های کاربری مختلف است، چرا خود IIS را روی کاربر جاری اجرا نکنیم؟ در این صورت ویژوال استودیو و اپلیکیشن ما هر دو به یک وهله از LocalDb وصل خواهند شد و همه چیز بدرستی کار خواهد کرد. ایجاد تغییرات لازم نسبتا ساده است. IIS را اجرا کنید و Application Pool مناسب را انتخاب کنید، یعنی همان گزینه که برای اپلیکیشن شما استفاده می‌شود.

          قسمت Advanced Settings را باز کنید:

          روی دکمه سه نقطه کنار خاصیت Identity کلیک کنید تا پنجره Application Pool Identity باز شود:

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

          خوب، معایب این رویکرد چیست؟ مسلما اجرای اپلیکیشن وب روی اکانت کاربری جاری، ریسک‌های امنیتی متعددی را معرفی می‌کند. اگر کسی بتواند اپلیکیشن وب ما را هک کند، به تمام منابع سیستم که اکانت کاربری جاری به آنها دسترسی دارد، دسترسی خواهد داشت. اما اجرای اپلیکیشن مورد نظر روی ApplicationPoolIdentity امنیت بیشتری را ارائه می‌کند، چرا که اکانت‌های ApplicationPoolIdentity دسترسی بسیار محدود‌تری به منابع سیستم محلی دارند. بنابراین استفاده از این روش بطور کلی توصیه نمی‌شود، اما در سناریو‌های خاصی با در نظر داشتن ریسک‌های امنیتی می‌تواند رویکرد خوبی باشد.


          رویکرد 2: استفاده از وهله مشترک

          یک راه حال دیگر استفاده از قابلیت instance sharing است. این قابلیت به ما این امکان را می‌دهد تا یک وهله LocalDb را بین کاربران یک سیستم به اشتراک بگذاریم. وهله به اشتراک گذاشته شده، توسط یک نام عمومی (public name) قابل دسترسی خواهد بود.

          ساده‌ترین راه برای به اشتراک گذاشتن وهله‌های LocalDb استفاده از ابزار SqlLocalDB.exe است. بدین منظور Command Prompt را بعنوان مدیر سیستم باز کنید و فرمان زیر را اجرا نمایید:

          sqllocaldb share v11.0 IIS_DB
          این فرمان وهله خصوصی LocalDb را با نام عمومی IIS_DB به اشتراک می‌گذارد. حال تمام کاربران سیستم می‌توانند با آدرس localdb)\.\IIS_DB) به این وهله وصل شوند. این فرمت آدرس دهی سرور دیتابیس، مشخص می‌کند که از یک وهله shared استفاده می‌کنیم. رشته اتصال جدید مانند لیست زیر خواهد بود:

          Data Source=(localdb)\.\IIS_DB;Initial Catalog=OldFashionedDB;Integrated Security=True

          پیش از آنکه اپلیکیشن وب ما بتواند به این وهله متصل شود، باید لاگین‌های مورد نیاز برای ApplicationPoolIdentity را ایجاد کنیم. راه اندازی وهله ساده است، کافی است دیتابیس را در SQL Server Object Explorer باز کنید. این کار اتصالی به دیتابیس برقرار می‌کند و آن را زنده نگاه می‌دارد. برای ایجاد لاگین مورد نظر، می‌توانیم در SQL Server Object Explorer یک کوئری اجرا کنیم:

          create login [IIS APPPOOL\ASP.NET v4.0] from windows;
          exec sp_addsrvrolemember N'IIS APPPOOL\ASP.NET v4.0', sysadmin

          اسکریپت بالا به اکانت ApplicationPoolIdentity سطح دسترسی کامل می‌دهد. در صورت امکان بهتر است از سطوح دسترسی محدود‌تری استفاده کنید، مثلا دسترسی به دیتابیس یا جداولی مشخص. حالا می‌توانید اپلیکیشن را مجددا اجرا کنید و همه چیز بدون خطا باید کار کند.

          معایب این روش چیست؟ مشکل اصلی در این رویکرد این است که پیش از آنکه اپلیکیشن ما بتواند به وهله مشترک دسترسی داشته باشد، باید وهله مورد نظر را راه اندازی و اجرا کنیم. بدین منظور، حساب کاربری ویندوزی که مالکیت وهله را دارد باید به آن وصل شود و کانکشن را زنده نگه دارد، در غیر اینصورت وهله LocalDb قابل دسترسی نخواهد بود.


          رویکرد 3: استفاده از SQL Server Express

          از آنجا که نسخه کامل SQL Server Express بعنوان یک سرویس ویندوزی اجرا می‌شود، شاید بهترین راه استفاده از همین روش باشد. کافی است یک نسخه از SQL Server Express را نصب کنیم، دیتابیس مورد نظر را در آن بسازیم و سپس به آن متصل شویم. برای این کار حتی می‌توانید از ابزار جدید SQL Server Data Tools استفاده کنید، چرا که با تمام نسخه‌های SQL Server سازگار است. در صورت استفاده از نسخه‌های کامل تر، رشته اتصال ما بدین شکل تغییر خواهد کرد:

          Data Source=.\SQLEXPRESS;Initial Catalog=OldFashionedDB;Integrated Security=True
          مسلما در این صورت نیز، لازم است اطمینان حاصل کنیم که ApplicationPoolIdentity به وهله SQL Server Express دسترسی کافی دارد. برای این کار می‌توانیم از اسکریپت قبلی استفاده کنیم:

          create login [IIS APPPOOL\ASP.NET v4.0] from windows;
          exec sp_addsrvrolemember N'IIS APPPOOL\ASP.NET v4.0', sysadmin

          حال اجرای مجدد اپلیکیشن باید با موفقیت انجام شود. استفاده از این روش مسلما امکان استفاده از LocalDb را از ما می‌گیرد. ناگفته نماند که وهله‌های SQL Server Express همیشه در حال اجرا خواهند بود چرا که بصورت سرویس‌های ویندوزی اجرا می‌شوند. همچنین استفاده از این روش ممکن است شما را با مشکلاتی هم مواجه کند. مثلا خرابی رجیستری ویندوز می‌تواند SQL Server Express را از کار بیاندازد و مواردی از این دست. راهکار‌های دیگری هم وجود دارند که در این مقاله به آنها نپرداختیم. مثلا می‌توانید از AttachDbFilename استفاده کنید یا از اسکریپت‌های T-SQL برای استفاده از وهله خصوصی ASP.NET کمک بگیرید. اما این روش‌ها دردسر‌های زیادی دارند، بهمین دلیل از آنها صرفنظر کردیم.


          مطالعه بیشتر درباره LocalDb

          مطالب
          مقدمه‌ای بر LINQ بخش اول
          کلمه‌ی LINQ مخفف Language Integrated Query یا زبان پرس و جوی یکپارچه می‌باشد. LINQ برای اولین بار در ویژوال استودیوی 2008 و دات نت فریم ورک 3.5 برای پرکردن خلع بین دنیای اشیاء برنامه نویسی (Object Oriented World) و دنیای داده‌ها (Data World) ارائه شد.

          چرا LINQ؟ 
          در نگاهی کلی، مزایایی که از طریق LINQ حاصل می‌شوند عبارتند از:
          • کاهش حجم کدنویسی 
          • درک بهتر از عملکرد کد‌های نوشته شده
          • پس از یادگیری اصول LINQ به راحتی می‌توانید از این اصول پرس و جو نویسی برای کار بر روی مجموعه داده‌های مختلف استفاده کنید
          • کنترل صحت کدهای پرس و جو‌ها در زمان کامپایل ( Compile-Time Type Checking )

          اجزای سازنده‌ی LINQ
          دو جزء اصلی سازنده‌ی LINQ عبارت است از:
          • Elements عناصر
          • Sequences توالی‌ها
          توالی‌ها می‌توانند لیستی از اطلاعات مختلف باشند. هر آیتم در لیست را عنصر می‌گوییم. توالی نمونه‌ای از یک کلاس است که اینترفیس <IEnumarable<T را پیاده سازی کرده باشد. این اینترفیس تضمین می‌کند که توالی قابلیت پیمایش عناصر را دارد.
          به آرایه‌ی تعریف شده‌ی زیر دقت کنید:
          int[] fibonacci = {0, 1, 1, 2, 3, 5};
          متغیر fibonacci در اینجا نشان دهنده‌ی توالی و هر یک از اعداد آرایه، یک عنصر محسوب می‌شوند.
          توالی می‌تواند درون حافظه‌ای باشد (In Memory Object) که به آن Local Sequence می‌گویند و یا می‌تواند یک بانک اطلاعاتی SQL Server باشد که به آن Remote Sequence می‌گویند.
          در حالت Remote باید اینترفیس <IQuerable<T پیاده سازی شده باشد.
          پرس و جو هایی را که بر روی توالی‌های محلی اجرا می‌شوند، اصطلاحا Local Query و یا LINQ-To-Object  نیز می‌نامند.
          عملگرهای پرس و جوی  زیادی به شکل متد الحاقی در کلاس System.Linq.Enumerable طراحی شده‌اند. این مجموعه از عملگرهای پرس جو را اصطلاحا Standard Query Operator می‌گویند.
          نکته‌ی مهم این است که عملگرهای پرس و جو تغییری را در توالی ورودی نمی‌دهند و نتیجه‌ی خروجی یک مجموعه جدید و یا یک مقدار عددی می‌باشد.

          توالی خروجی و مقدار بازگشتی Scalar
          در بخش قبل گفتیم که خروجی یک پرس و جو می‌تواند یک مجموعه و یا یک مقدار عددی باشد. در مثال زیر عملگر Count را بر روی مجموعه‌ی fibonacci  اعمال کردیم و عددی که نشان دهنده‌ی تعداد عناصر مجموعه است، بعنوان خروجی بازگردانده شده است.
          int[] fibonacci = { 0, 1, 1, 2, 3, 5 };
          int numberOfElements = fibonacci.Count();
          Console.WriteLine($"{numberOfElements}");
          IEnumerable<int> distinictNumbers = fibonacci.Distinct();
          Console.WriteLine("Elements in output sequence:");
          foreach (var number in distinictNumbers)
          {
              Console.WriteLine(number);
          }
          در کد بالا توسط تابع Distinct، عناصر یکتا را از توالی ورودی استخراج کرده و بازگردانده‌ایم.
          خروجی برنامه‌ی فوق به شکل زیر است :
          6
          Elements in output sequence:
          0
          1
          2
          3
          5

          مفهوم Deffer Execution  (
          اجرای به تاخیر افتاده )
          عمده‌ی عملگر‌های پرس و جو بلافاصله پس از ایجاد، اجرا نمی‌شوند. این عملگرها در طول اجرای برنامه اجرا خواهند شد (اجرای با تاخیر). به همین خاطر می‌توان بعد از ساخت پرس و جو  تغییرات دلخواهی را به توالی ورودی اعمال کرد.
          در کد زیر  قبل از اجرای پرس و جو ، توالی ورودی ویرایش شده :
          int[] fibonacci = { 0, 1, 1, 2, 3, 5 };
          // ایجاد پرس و جو 
          IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2);
          // در این مرحله پرس و جو ایجاد شده ولی هنوز اجرا نشده است
          // تغییر عنصر اول توالی
          fibonacci[0] = 99;
          // حرکت بر روی عناصر توالی باعث اجرای پرس و جو می‌شود
          foreach (var number in numbersGreaterThanTwoQuery)
          {
             Console.WriteLine(number);
          }
          پرس و جو تا زمان اجرای حلقه‌ی Foreach اجرا نخواهد شد. خروجی مثال بالا به شکل زیر است :
          99
          3
          5

          به غیر از بعضی از عملگرها مثل Count,Min,Last سایر عملگر‌ها بصورت اجرای با تاخیر عمل می‌کنند. عملگری مثل Count باعث اجرای فوری پرس و جو می‌شود.
          تعدادی عملگر تبدیل (Conversion Operator) هم وجود دارد که باعث می‌شوند پرس و جو بلافاصله اجرا شود :
          • ToList
          • ToArray
          • ToLookup
          • ToDictionary
          عملگر‌های فوق پس از اجرا، خروجی را در یک ساختمان داده‌ی جدید باز می‌گردانند.
          در کد زیر اصلاح توالی متغیر Fibonacci بعد از اجرای تابع ToArray صورت گرفته است.
          int[] fibonacci = { 0, 1, 1, 2, 3, 5 };
          // ساخت پرس و جو
          IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2) .ToArray();
          // در این مرحله به خاطر عملگر استفاده شده پرس و جو اجرا می‌شود
          // تغییر اولین عنصر توالی
          fibonacci[0] = 99;
          // حرکت بر روی نتیجه
          foreach (var number in numbersGreaterThanTwoQuery)
          {
             Console.WriteLine(number);
          }
          خروجی مثال بالا:
          3
          5
          همانطور که می‌بینید عدد 99 در خروجی مشاهده نمی‌شود. علت فراخوانی عملگر ToArray است که بلافاصله باعث اجرای پرس و جو شده و خروجی را باز می‌گرداند . به همین خاطر تغییر عنصر اول توالی ورودی، تاثیری بر روی نتیجه‌ی خروجی ندارد. 
          نظرات مطالب
          آموزش MDX Query - قسمت شانزدهم – استفاده از تابع Filter در MDX Query ها
          با استفاده از ADO.NET نمی‌توان کوئری‌های MDX را مستقیما اجرا کرد. برای اینکار نیاز به Microsoft.AnalysisServices.AdomdClient.dll هست. برای دریافت آن به صفحه‌ی Microsoft® SQL Server® 2012 Feature Pack مراجعه و قسمت Microsoft® SQL Server® 2012 ADOMD.NET را در صفحه جستجو کنید. برای نگارش 2008 به صفحه‌ی Microsoft® SQL Server® 2008 R2 Feature Pack مراجعه و در آن Analysis Management Objects را دریافت کنید. برای SQL Server 2005 نیز در اینجا پروایدر ADOMD قابل دریافت است.
          بعد برای استفاده از آن خواهید داشت:
          using (AdomdConnection conn = 
                       new AdomdConnection("Data Source=tfsDB;Initial Catalog=Tfs_Analysis; MDX Compatibility=1;")) 
              {  
                  conn.Open();  
                  using (AdomdCommand cmd = new AdomdCommand(" ....... mdxQuery here ....... ", conn))  
                  {  
                      DataSet ds = new DataSet();  
                      ds.EnforceConstraints = false;  
                      ds.Tables.Add();  
                      DataTable dt = ds.Tables[0];  
                      dt.Load(cmd.ExecuteReader());  
                      return dt;  
                  }  
              }
          و یا پروژه MdxClient - ADO.NET data provider for MDX queries سعی کرده این روش را تبدیل به یک پروایدر جدید ADO.NET کند. نکته‌ی جالب این پروایدر امکان تعریف مقادیر دریافتی از کاربر به صورت پارامتر است در کوئری‌ها تعریف شده. فایل readme آن‌را برای مشاهده یک سری مثال در مورد نحوه‌ی تعریف پارامترها و دریافت داده‌های ارسالی از طریق کاربر، مطالعه کنید.
          مطالب دوره‌ها
          تراکنش‌ها در RavenDB
          پیش از شروع به بحث در مورد تراکنش‌ها و نحوه مدیریت آن‌ها در RavenDB، نیاز است با مفهوم ACID آشنا شویم.

          ACID چیست؟

          ACID از 4 قاعده تشکیل شده است (Atomic, Consistent, Isolated, and Durable) که با کنار هم قرار دادن آن‌ها یک تراکنش مفهوم پیدا می‌کند:

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

          ب) Consistent یا یکپارچه
          در اینجا consistency علاوه بر اعمال قیود، به معنای اطلاعاتی است که بلافاصله پس از پایان تراکنشی از سیستم قابل دریافت و خواندن است.

          ج) Isolated: محصور شده
          اگر چندین تراکنش در یک زمان با هم در حال اجرا باشند، نتیجه نهایی با حالتی که تراکنش‌ها یکی پس از دیگری اجرا می‌شوند باید یکی باشد.

          د) Durable: ماندگار
          اگر سیستم پایان تراکنشی را اعلام می‌کند، این مورد به معنای 100 درصد نوشته شدن اطلاعات در سخت دیسک باید باشد.


          مراحل چهارگانه ACID در RavenDB به چه نحوی وجود دارند؟

          RavebDB از هر دو نوع تراکنش‌های implicit و explicit پشتیبانی می‌کند. Implicit به این معنا است که در حین استفاده معمول از RavenDB (و بدون انجام تنظیمات خاصی)، به صورت خودکار مفهوم تراکنش‌ها وجود داشته و اعمال می‌شوند. برای نمونه به متد ذیل توجه نمائید:
          public void TransferMoney(string fromAccountNumber, string toAccountNumber, decimal amount) 
          {
             using(var session = Store.OpenSession()) 
             {
                   session.Advanced.UseOptimisticConcurrency = true;
          
                   var fromAccount = session.Load<Account>("Accounts/" + fromAccountNumber);
                   var toAccount = session.Load<Account>("Accounts/" + toAccountNumber);
          
                   fromAccount.Balance -= amount;
                   toAccount.Balance += amount;
          
                   session.SaveChanges();
             }
          }
          در این متد مراحل ذیل رخ می‌دهند:
          - از document store ایی که پیشتر تدارک دیده شده، جهت بازکردن یک سشن استفاده شده است.
          - به سشن صراحتا عنوان شده است که از Optimistic Concurrency استفاده کند. در این حالت RavenDB اطمینان حاصل می‌کند که اکانت‌های بارگذاری شده توسط متدهای Load، تا زمان فراخوانی SaveChanges تغییر پیدا نکرده‌اند (و در غیراینصورت یک استثناء را صادر می‌کند).
          - دو اکانت بر اساس Id آن‌ها از بانک اطلاعاتی واکشی می‌شوند.
          - موجودی یکی تقلیل یافته و موجودی دیگر، افزایش می‌یابد.
          - متد SaveChanges بر روی شی‌ء سشن فراخوانی شده است. تا زمانیکه این متد فراخوانی نشده است، کلیه تغییرات در حافظه نگهداری می‌شوند و به سرور ارسال نخواهند شد. فراخوانی آن سبب کامل شدن تراکنش و ارسال اطلاعات به سرور می‌گردد.
          بنابراین شیء سشن بیانگر یک atomic transaction ماندگار و محصور شده است (سه جزء ACID تاکنون محقق شده‌اند). محصور شده بودن آن به این معنا است که:
          الف) هر تغییری که در سشن اعمال می‌شود، تا پیش از فراخوانی متد SaveChanges از دید سایر تراکنش‌ها مخفی است.
          ب) اگر دو تراکنش همزمان رخ دهند، تغییرات هیچکدام بر روی دیگری اثری ندارد.

          اما Consistency یا یکپارچگی در RavenDB بستگی دارد به نحوه‌ی خواندن اطلاعات و این مورد با دنیای رابطه‌ای اندکی متفاوت است که در ادامه جزئیات آن‌را بیشتر بررسی خواهیم کرد.


          عاقبت یک دست شدن یا eventual consistency

          درک Consistency مفهوم ACID در RavenDB بسیار مهم است و عدم آشنایی با نحوه عملکرد آن می‌تواند مشکل‌ساز شود. در دنیای بانک‌های اطلاعاتی رابطه‌ای، برنامه نویس‌ها به «immediate consistency» عادت دارند (یکپارچگی آنی). به این معنا که هرگونه تغییری در بانک اطلاعاتی، پس از پایان تراکنش، بلافاصله در اختیار کلیه خوانندگان سیستم قرار می‌گیرد. در RavenDB و خصوصا دنیای NoSQL، این یکپارچگی آنی دنیای رابطه‌ای، به «eventual consistency» تبدیل می‌شود (عاقبت یک‌دست شدن). عاقبت یک دست شدن در RavenDB به این معنا است که اگر تغییری به یک سند اعمال گردیده و ذخیره شود؛ کوئری انجام شده بر روی این اطلاعات تغییر یافته ممکن است «stale data» باز گرداند. واژه stale در RavenDB به این معنا است که هنوز اطلاعاتی در دیتابیس موجود هستند که جهت تکمیل ایندکس‌ها پردازش نشده‌اند. به این مورد در قسمت بررسی ایندکس‌ها در RavenDB اشاره شد.
          در RavenDB یک سری تردهای پشت صحنه، مدام مشغول به کار هستند و بدون کند کردن عملیات سیستم، کار ایندکس کردن اطلاعات را انجام می‌دهند. هر زمانیکه اطلاعاتی را ذخیره می‌کنیم، بلافاصله این تردها تغییرات را تشخیص داده و ایندکس‌ها را به روز رسانی می‌کنند. همچنین باید درنظر داشت که RavenDB جزو معدود بانک‌های اطلاعاتی است که خودش را بر اساس نحوه استفاده شما ایندکس می‌کند! (نمونه‌ای از آن‌را در قسمت ایندکس‌های پویای حاصل از کوئری‌های LINQ پیشتر مشاهده کرده‌اید)

          نکته مهم
          در RavenDB اگر از کوئری‌های LINQ استفاده کنیم، ممکن است به علت اینکه هنوز تردهای پشت صحنه‌ی ایندکس سازی اطلاعات، کارشان تمام نشده است، تمام اطلاعات یا آخرین اطلاعات را دریافت نکنیم (که به آن stale data گفته می‌شود). هر آنچه که ایندکس شده است دریافت می‌گردد (مفهوم عاقبت یک دست شدن ایندکس‌ها). اما اگر نیاز به یکپارچگی آنی داشتیم، متد Load یک سشن، مستقیما به بانک اطلاعاتی مراجعه می‌کند و اطلاعات بازگشت داده شده توسط آن هیچگاه احتمال stale بودن را ندارند.
          بنابراین برای نمایش اطلاعات یا گزارشگیری، از کوئری‌های LINQ استفاده کنید. RavenDB خودش را بر اساس کوئری شما ایندکس خواهد کرد و نهایتا به کوئری‌هایی فوق العاده سریعی در طول کارکرد سیستم خواهیم رسید. اما در صفحه ویرایش اطلاعات بهتر است از متد Load استفاده گردد تا نیاز به مفهوم immediate consistency یا یکپارچگی آنی برآورده شود.


          تنظیمات خاص کار با ایندکس سازها برای انتظار جهت اتمام کار آن‌ها

          عنوان شد که اگر ایندکس سازهای پشت صحنه هنوز کارشان تمام نشده است، در حین کوئری گرفتن، هر آنچه که ایندکس شده بازگشت داده می‌شود.
          در اینجا می‌توان به RavenDB گفت که تا چه زمانی می‌تواند یک کوئری را جهت دریافت اطلاعات نهایی به تاخیر بیندازد. برای اینکار باید اندکی کوئری‌های LINQ آن‌را سفارشی سازی کنیم:
          RavenQueryStatistics stats;
          var results = session.Query<Product>()
              .Statistics(out stats)
              .Where(x => x.Price > 10)
              .ToArray();
           
          if (stats.IsStale)
          {
              // Results are known to be stale
          }
          توسط امکانات آماری کوئری‌های LINQ در RavenDB مطابق کدهای فوق، می‌توان دریافت که آیا اطلاعات دریافت شده stale است یا خیر.
          همچنین زمان انتظار تا پایان کار ایندکس ساز را نیز توسط متد Customize به نحو ذیل می‌توان تنظیم کرد:
          RavenQueryStatistics stats;
          var results = session.Query<Product>()
              .Statistics(out stats)
              .Where(x => x.Price > 10)
              .Customize(x => x.WaitForNonStaleResults(TimeSpan.FromSeconds(5)))
              .ToArray();
          به علاوه می‌توان کلیه کوئری‌های یک documentStore را وارد به صبر کردن تا پایان کار ایندکس سازی کرد (متد Customize پیش فرضی را با WaitForNonStaleResultsAsOfLastWrite مقدار دهی و اعمال می‌کند):
           documentStore.Conventions.DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites;
          این مورد در برنامه‌های وب توصیه نمی‌شود چون کل سیستم در حین آغاز کار با آن بر اساس یک documentStore سینگلتون باید کار کند و همین مساله صبر کردن‌ها، با بالا رفتن حجم اطلاعات و تعداد کاربران، پاسخ دهی سیستم را تحت تاثیر قرار خواهد داد. به علاوه این تنظیم خاص بر روی کوئری‌های پیشرفته Map/Reduce کار نمی‌کند. در این نوع کوئری‌های ویژه، برای صبر کردن تا پایان کار ایندکس شدن، می‌توان از روش زیر استفاده کرد:
          while (documentStore.DatabaseCommands.GetStatistics().StaleIndexes.Length != 0)
          {
              Thread.Sleep(10);
          }

          مقابله با تداخلات همزمانی

          با تنظیم session.Advanced.UseOptimisticConcurrency = true، اگر سندی که در حال ویرایش است، در این حین توسط کاربر دیگری تغییر کرده باشد، استثنای ConcurrencyException صادر خواهد شد. همچنین این استثناء در صورتیکه شخصی قصد بازنویسی سند موجودی را داشته باشد نیز صادر خواهد شد (شخصی بخواهد سندی را با ID سند موجودی ذخیره کند). اگر از optimistic concurrency استفاده نشود، آخرین ترد نویسنده یا به روز کننده اطلاعات، برنده خواهد شد و اطلاعات نهایی موجود در بانک اطلاعاتی متعلق به او و حاصل بازنویسی آن ترد است.
           optimistic concurrency به زبان ساده به معنای به خاطر سپردن شماره نگارش یک سند است، زمانیکه آن‌را بارگذاری می‌کنیم و سپس ارسال آن به سرور، زمانیکه قصد ذخیره آن‌را داریم. در SQL Server اینکار توسط RowVersion انجام می‌شود. در بانک‌های اطلاعاتی سندگرا چون تمایل به استفاده از HTTP در آن‌ها زیاد است (مانند RavenDB) از مکانیزمی به نام E-Tag برای این منظور کمک گرفته می‌شود. هر زمانیکه تغییری به یک سند اعمال می‌شود، E-Tag آن  به صورت خودکار افزایش خواهد یافت.
          برای مثال فرض کنید کاربری سندی را با E-Tag مساوی 2 بارگذاری کرده است. قبل از اینکه این کاربر در صفحه ویرایش اطلاعات کارش با این سند خاتمه یابد، کاربر دیگری در شبکه، این سند را ویرایش کرده است و اکنون E-Tag آن مثلا مساوی 6 است. در این زمان اگر کاربر یک سعی به ذخیره سازی اطلاعات نماید، چون E-Tag سند او با E-Tag سند موجود در سرور دیگر یکی نیست، با استثنای ConcurrencyException متوقف خواهد شد.



          مشکل! در برنامه‌های بدون حالت وب، چون پس از نمایش صفحه ویرایش اطلاعات، سشن RavenDB نیز بلافاصله Dispose خواهد شد، این E-Tag را از دست خواهیم داد. همچنین باید دقت داشت که سشن RavenDB به هیچ عنوان نباید در طول عمر یک برنامه باز نگهداشته شود و برای طول عمری کوتاه طراحی شده است. راه حلی که برای آن درنظر گرفته شده است، ذخیره سازی این E-Tag در بار اول دریافت آن از سشن می‌باشد. برای این منظور تنها کافی است خاصیتی را به نام Etag با ویژگی JsonIgnore (که سبب عدم ذخیره سازی آن در بانک اطلاعاتی خواهد شد) تعریف کنیم:
          public class Person
          {
              public string Id { get; set; }
          
              [JsonIgnore]
              public Guid? Etag { get; set; }
          
              public string Name { get; set; }
          }
          اکنون زمانیکه سندی را از بانک اطلاعاتی دریافت می‌کنیم، با استفاده از متد session.Advanced.GetEtagFor، می‌توان این Etag واقعی را دریافت کرد و ذخیره نمود:
          public Person Get(string id)
          {
              var person = session.Load<Person>(id);
              person.Etag = session.Advanced.GetEtagFor(person);
              return person;
          }
          و برای استفاده از آن ابتدا باید UseOptimisticConcurrency به true تنظیم شده و سپس در متد Store این Etag دریافتی از سرور را مشخص نمائیم:
          public void Update(Person person)
          {
              session.Advanced.UseOptimisticConcurrency = true;
              session.Store(person, person.Etag, person.Id);
              session.SaveChanges();
              person.Etag = session.Advanced.GetEtagFor(person);
          }


          تراکنش‌های صریح

          همانطور که عنوان شد، به صورت ضمنی کلیه سشن‌ها، یک واحد کار را تشکیل داده و با پایان آن‌ها، تراکنش خاتمه می‌یابد. اگر به هر علتی قصد تغییر این رفتار ضمنی پیش فرض را دارید، امکان تعریف صریح تراکنش‌های نیز وجود دارد:
          using (var transaction = new TransactionScope())
          {
             using (var session1 = store.OpenSession())
             {
               session1.Store(new Account());
               session1.SaveChanges();
             }
          
             using (var session2 = store.OpenSession())
             {
               session2.Store(new Account());
               session2.SaveChanges();
             }
          
             transaction.Complete();
          }
          باید دقت داشت که پایان یک تراکنش، یک non-blocking asynchronous call است و مباحث stale data که پیشتر در مورد آن بحث شد، برقرار هستند.