مطالب
آموزش Linq - بخش ششم : عملگرهای پرس و جو قسمت سوم
عملگر‌های تبدیل Conversion Operator

عملگر‌های پرس و جوی تبدیل، توالی‌هایی را که از جنس <IEnumerable<T هستند، به انواع دیگر مجموعه تبدیل می‌کنند.
از عملگر‌های پرس و جوی زیر می‌توان برای تبدیل توالی‌ها استفاده کرد :
  • OfType
  • Cast
  • ToArray
  • ToList
  • ToDictionary
  • ToLookup

عملگر OfType


این عملگر عناصری از توالی را که نوع آنها را مشخص می‌کنیم باز می‌گرداند.
امضاء عملگر پرس و جوی OfType  به صورت زیر است :
 public static IEnumerable<TResult> OfType<TResult>(this IEnumerable source)
همانطور که مشاهده می‌کنید توالی ورودی از یک نوع IEnumerable غیر جنریک می‌باشد. بدین معنی که عناصر توالی ورودی می‌توانند از نوع داده‌های مختلف باشند (توالی از اشیاء، از جنس Object).
در مثال زیر یک توالی IEnumerable (آرایه‌ای از اشیاء)، از عناصر با نوع داده‌های مختلفی را ایجاد کرده‌ایم. عملگر OfType در اینجا کلیه عناصر از جنس (string) را باز می‌گرداند. توالی خروجی یک نوع IEnumerable جنریک است(در این مثال <IEnumerable<List).
مثال :
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime()};
IEnumerable<string> query = input.OfType<string>();
foreach (var item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا :
Apple
Sugar
عملگر OfType را می‌توان به‌همراه  Strongly Type‌‌ها نیز استفاده کرد.
مثال :کد زیر یک ساختار سلسله مراتبی شیء گرا را نمایش می‌دهد:
 class Ingredient
  {
     public string Name { get; set; }
  }
  class DryIngredient : Ingredient
  {
     public int Grams { get; set; }
  }

  class WetIngredient : Ingredient
  {
     public int Millilitres { get; set; }
  }
کد زیر چگونگی استفاده از OfType را برای بدست آوردن یک زیر نوع (Subtype) مشخص، نشان می‌دهد (در این مثال، نوع WetIngredient):
IEnumerable<Ingredient> input = new Ingredient[]
{
   new DryIngredient { Name = "Flour" },
   new WetIngredient { Name = "Milk" },
   new WetIngredient { Name = "Water" }
};

IEnumerable<WetIngredient> query = input.OfType<WetIngredient>();
foreach (WetIngredient item in query)
{
   Console.WriteLine(item.Name);
}
خروجی مثال بالا :
Milk
Water

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر Cast


عملگر Cast همانند عملگر OfType رفتار می‌کند. این عملگر یک توالی ورودی را دریافت و بر اساس نوع مشخص شده، توالی خروجی را تولید می‌کند. همه‌ی عناصر توالی ورودی به نوع مشخص شده Cast می‌شوند. اما بر عکس عملگر OfType که عناصری را که با نوع داده‌ی ما سازگاری نداشت، نادیده می‌گرفت، این عملگر در صورت عدم موفقیت در عملیات تغییر نقش (Cast)، یک استثناء را پرتاب می‌کند.
مثال : 
IEnumerable input = new object[]
{
   "Apple", 33, "Sugar", 44, 'a', new DateTime()
};

IEnumerable<string> query = input.Cast<string>();
foreach (string item in query)
{
   Console.WriteLine(item);
}
با اجرای برنامه‌ی فوق، خطای زیر را مشاهده خواهید کرد:
 Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.

پیاده سازی توسط عبارت‌های جستجو


کلمه‌ی کلیدی جایگزینی برای عملگر Cast، در عبارت‌های جستجو وجود ندارد.این عملگر با استفاده از متغیر Range که در مطالب قبلی این سری معرفی شد، قابل پیاده سازی می‌باشد.
IEnumerable input = new object[]{ "Apple", "Sugar", "Flour" };
IEnumerable<string> query =
from string i in input
select i;

foreach (var item in query)
{
   Console.WriteLine(item);
}
نکته:  در مثال فوق تعریف صریح (Explicit) نوع داده، قبل از متغیر Range انجام شده است (معادل همان نوع داده در عملیات Cast).


عملگر ToArray


عملگر ToArray یک توالی ورودی را دریافت و یک توالی خروجی را به صورت آرایه تولید می‌کند. این عملگر باعث اجرای سریع پرس و جو می‌شود و رفتار پیش فرض LINQ را که اجرای با تاخیر می‌باشد، تحریف/بازنویسی (Override) می‌کند.
مثال: در این مثال یک توالی از نوع <IEnumerable<string به یک آرایه رشته‌ای تبدیل شده است (تبدیل لیست به آرایه).
 IEnumerable<string> input = new List<string> { "Apple", "Sugar", "Flour" };
string[] array = input.ToArray();

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ToList

عملگر ToList همچون ToArray، اجرای با تاخیر را نادیده می‌گیرد. عملگر ToList همانطور که از نامش پیداست، توالی خروجی را به‌صورت لیست مهیا می‌کند.
مثال:
 IEnumerable<string> input = new[] { "Apple", "Sugar", "Flour" };
List<string> list = input.ToList();

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ToDictionary

این عملگر توالی ورودی را به یک  دیکشنری جنریک تبدیل می‌کند (<Dictinary<TKey,TValue) .
ساده‌ترین امضاء عملگر ToDictionary، یک عبارت Lambda می‌باشد. این عبارت Lambda  نشان دهنده‌ی یک تابع است که عنصر کلید(Key) را در دیکشنری، مشخص می‌کند.
مثال:
class Recipe
{
   public int Id { get; set; }
   public string Name { get; set; }
   public int Rating { get; set; }
}

IEnumerable<Recipe> recipes = new[]
{
   new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 },
   new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 },
   new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 }
};

Dictionary<int, Recipe> dict = recipes.ToDictionary(x => x.Id);
foreach (KeyValuePair<int, Recipe> item in dict)
{
   Console.WriteLine($"Key={item.Key}, Recipe={item.Value}");
}
در کد بالا ، کلید دیکشنری نهایی، از نوع  int می‌باشد که بر اساس Id کلاس Recipe تنظیم شده است. مقادیر (value) دیکشنری هم همان اشیاء از جنس کلاس Recipe می‌باشند.
خروجی مثال بالا:
Key=1, Recipe=Apple Pie
Key=2, Recipe=Cherry Pie
Key=3, Recipe=Beef Pie

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ToLookup


این عملگر رفتاری شبیه به عملگر ToDictionary را دارد، اما به جای تولید خروجی از نوع دیکشنری، نمونه‌ای از جنس ILookUp را ایجاد می‌کند.
در کد زیر خروجی ایجاد شده توسط lookup دستورالعمل‌ها (Recipes) را بر حسب  امتیاز آنها گروه بندی کرده است. در این مثال کلید، بر حسب Byte می‌باشد.
مثال :
class Recipe
{
   public int Id { get; set; }
   public string Name { get; set; }
   public byte Rating { get; set; }
}

IEnumerable<Recipe> recipes = new[]
{
   new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 },
   new Recipe { Id = 1, Name = "Banana Pie", Rating = 5 },
   new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 },
   new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 }
};

ILookup<byte, Recipe> look = recipes.ToLookup(x => x.Rating);
foreach (IGrouping<byte, Recipe> ratingGroup in look)
{
   byte rating = ratingGroup.Key;
   Console.WriteLine($"Rating {rating}");
   foreach (var recipe in ratingGroup)
   {
      Console.WriteLine($" - {recipe.Name}");
   }
}
خروجی مثال بالا:
 Rating 5
 - Apple Pie
 - Banana Pie
Rating 2
 - Cherry Pie
Rating 3
 - Beef Pie

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر‌های عناصر  Element Operators

این عملگر‌ها، یک توالی ورودی را دریافت و تنها یک عنصر از توالی ورودی و یا یک عنصر را به عنوان عنصر پیش فرض باز می‌گردانند. این نوع عملگر‌ها توالی خروجی را تولید نمی‌کنند.


عملگر First

این عملگر اولین عنصر توالی را باز می‌گرداند.
مثال :
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};

Ingredient element = ingredients.First();
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Sugar
امضای دیگر این متد، امکان تعریف یک شرط را مهیا می‌کند. خروجی این حالت اولین عنصری است که شرط را تامین می‌کند. در کد زیر اولین عنصری که کالری آن برابر 150 باشد به خروجی ارسال می‌شود.
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};

Ingredient element = ingredients.First(x=>x.Calories==150);
Console.WriteLine(element.Name);
خروجی مثال بالا:
 Milk
در زمان استفاده از عملگر First، اگر توالی ورودی هیچ عنصری نداشته باشد، یک استثناء رخ خواهد داد:
 Unhandled Exception: System.InvalidOperationException: Sequence contains no elements
کد زیر نمونه‌ای از این حالت است:
Ingredient[] ingredients = { };
Ingredient element = ingredients.First();
در زمان استفاده‌ی از امضاء دیگر عملگر First، اگر هیچ عنصری شرط معرفی شده‌ی در پارامتر را تامین نکند، باز هم یک استثناء رخ خواهد داد:
 Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
کد زیر حالت فوق را نشان می‌دهد:
 Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};
Ingredient element = ingredients.First(x=>x.Calories==1500);

پیاده سازی توسط عبارت‌های جستجو

معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر FirstOrDefault

عملگر FirstOrDefalt همانند عملگر First عمل می‌کند، اما با این تفاوت که به جای پرتاب یک استثناء در شرایط معرفی شده در عملگر First، یک مقدار پیش فرض را بر اساس نوع  عناصر توالی باز می‌گرداند. در صورتیکه توالی از نوع عددی باشد، مقدار 0 و اگر عناصر توالی از انواع ارجاعی باشند، مقدار Null و برای مقادیر منطقی، ارزش False به‌عنوان مقادیر پیش فرض باز گردانده می‌شوند.
مثال :
 Ingredient[] ingredients = { };
Ingredient element = ingredients.FirstOrDefault();
Console.WriteLine(element == null);
خروجی مثال بالا :
 True
پیاده سازی حالتی که هیچ یک از عناصر با شرط عملگر کطالبقت ندارند.
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};

Ingredient element = ingredients.FirstOrDefault(x=>x.Calories==1500);
Console.WriteLine(element==null);
خروجی مثال بالا :
 True

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


 عملگر Last

این عملگر آخرین عنصر توالی را باز می‌گرداند. همچون عملگر First، این عملگر نیز یک امضاء برای دریافت یک عبارت شرط یا پیش بینی دارد. این پیش بینی، آخرین عنصری را که شرط را تامین کند، باز می‌گرداند. باز هم مثل عملگر First، در صورتی که توالی هیچ عنصری نداشته باشد و یا عدم تامین شرط توسط عناصر توالی، استثنایی رخ خواهد داد.
مثال :
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 500}
};
Ingredient element = ingredients.Last(x=>x.Calories==500);
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Flour

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر LastOrDefault

این عملگر همچون عملگر FirstOrDefault عمل می‌کند. از بروز استثناء جلوگیری کرده و مقدار پیش فرض را به خروجی ارسال می‌کند.

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر Single

عملگر Single ، تنها عنصر توالی ورودی را باز می‌گرداند.در صورتی که توالی ما بیش از یک عنصر داشته باشد و یا توالی هیچ عنصری نداشته باشد، یک استثناء رخ خواهد داد.
Unhandled Exception: System.InvalidOperationException: Sequence contains more than one matching element
Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
مثال :
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 }
};

Ingredient element = ingredients.Single();
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Sugar
عملگر Single، یک امضاء دیگر نیز دارد که یک عبارت پیش بینی را می‌پذیرد. در صورتی که بیش از یک عنصر، با پیش بینی مطابقت داشته باشد و یا هیچ عنصری شرط پیش بینی را تامین نکند، استثنائی رخ خواهد داد.
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Butter", Calories = 150},
   new Ingredient {Name = "Milk", Calories = 500}
};
Ingredient element = ingredients.Single(x => x.Calories == 150);
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Butter

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر SingleOrDefault

عملگر SingleOrDefault همچون عملگر Single عمل می‌کند؛ اما با این تفاوت که اگر توالی هیچ عنصری نداشته باشد، مقدار پیش فرض نوع توالی، باز گردانده می‌شود و در صورتیکه هیچ عنصری شرط مشخص شده را تامین نکند، باز هم مقدار پیش فرض توالی، به جای رخ دادن استثناء باز گردانده می‌شود.
مثال : در این مثال هیچ عنصری با پیش بینی مشخص شده مطالبقت ندارد:
 Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};
Ingredient element = ingredients.SingleOrDefault(x => x.Calories == 9999);
Console.WriteLine(element==null);
خروجی مثال بالا :
True
توجه داشته باشید که استثنائی رخ نداده است و مقدار پیش فرض انواع ارجاعی که Null می‌باشد باز گردانده شده است.

پیاده سازی توسط عبارت‌های جستجو

معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ElementAt


عملگر ElementAt   عنصری را در یک جایگاه مشخص شده‌ی در توالی، باز می‌گرداند.
مثال: در کد زیر سومین عنصر توالی ورودی انتخاب می‌شود:
 Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};

Ingredient element = ingredients.ElementAt(2);
Console.WriteLine(element.Name);
خروجی مثال بالا :
 Milk
باید دقت کرد که مقدار ارسالی به عملگر  ElementAt، اندیسی با نقطه‌ی آغاز صفر می‌باشد. بدین معنی که برای بدست آوردن اولین عنصر باید مقدار 0 را به عملگر ElementAt ارسال کرد. در صورتی که مقدار ارسالی با بازه اندیس‌های عناصر توالی مطابقت نداشته باشد (بزرگتر از شماره اندیس آخرین عنصر توالی باشد) یک استثناء رخ خواهد داد.
 System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر ElementAtOrDefualt

عملگر ElementAtOrDefualt نیز همچون عملگر ElementAt کار می‌کند؛ اما در صورت وارد کردن اندیسی بزرگتر از اندیس مجاز توالی، دیگر یک استثناء رخ نخواهد داد و یک مقدار پیش فرض، بر اساس نوع عناصر توالی باز گردانده می‌شود.
مثال :
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};
Ingredient element = ingredients.ElementAtOrDefault(5);
Console.WriteLine(element==null);
خروجی مثال بالا:
 True

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.


عملگر DefaultIfEmpty
عملگر DefaultIfEmpty یک توالی را دریافت کرده و به دو شکل عمل می‌کند:
1- اگر توالی شامل حداقل یک عنصر باشد، این توالی بدون هیچ تغییری به خروجی ارسال می‌شود.
2- اگر توالی هیچ عنصری نداشته باشد، توالی خروجی خالی نخواهد بود. در این حالت توالی خروجی تنها یک عضو دارد و آن هم مقدار پیش فرضی بر اساس نوع توالی می‌باشد.
مثال :
Ingredient[] ingredients =
{
   new Ingredient { Name = "Sugar", Calories = 500 },
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 50}
};

IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty();
foreach (Ingredient item in query)
{
  Console.WriteLine(item.Name);
}
خروجی مثال بالا :
Sugar
Egg
Milk
همانطور که می‌بینید توالی خروجی دقیقا شبیه توالی ورودی می‌باشد.
کد زیر حالت دوم معرفی شده‌ی در تعریف DefaultIfEmpty را نشان می‌دهد.
Ingredient[] ingredients = { };
IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty();
foreach (Ingredient item in query)
{
   Console.WriteLine(item == null);
}
خروجی کد بالا :
 True

پیاده سازی توسط عبارت‌های جستجو


معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.
مطالب
استفاده از چند فرم در کنار هم در ASP.NET MVC

اجرای این نوع صفحات کار سختی نیست؛ با کمی جستجو در اینترنت مثلا در اینجا میتوانید چیزهای خوبی پیدا کنید. اما متاسفانه اکثر مثال‌ها چیزی شبیه قرار دادن پارشال "ورود اعضا" در کنار پارشال "ثبت نام" هستند. حتما متوجه شده‌اید که معمولا این دو صفحه پس از  PostBack به صفحه‌ای جدید هدایت میشوند و یا در بهترین حالت به کمک Ajax ، پس از انجام عملیات، پیامی به کاربر نمایش میدهیم.

در این مقاله سعی شده روشی برای ایجاد چند فرم در یک View توضیح داده شود با این شرط که: 

اولا : از Ajax یا هلپر ایجکسی استفاده نکنیم.

ثانیا : پس از post-back، عملیات Redirect را انجام ندهیم و صفحه جاری را حفظ کنیم؛ چه قرار باشد همه چیز درست انجام شده باشد و چه مشکلی پیش آمده باشد و پیام خطایی در کنار فیلد‌ها نمایش داده شود. 

 در این روش به این نکته توجه شده که هر مدل پس از Post-back حفظ شود و مستقل از دیگری رفتار کند. مثلا اگر یکی از فرم‌ها ناقص پر شد و دکمه‌ی ارسال آن فشرده شد، پس از Post-back، فقط و فقط اجزای همین فرم Validate شود و فرم دوم بدون تغییر باقی بماند. 

ویوی زیر را در نظر بگیرید. در layout، دو پارشال، به کمک اکشن‌متد فراخوانی شده‌اند:

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

ContactVM .cs  

public class ContactVM
    {
        [Display(Name = "نام")]
        [Required(ErrorMessage = "لطفا {0} را وارد کنید")]
        public string Name { get; set; }

              [EmailAddress(ErrorMessage = "آدرس ایمیل صحیح نیست")]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "آدرس ایمیل")]
        [Required(ErrorMessage = "لطفا {0} را وارد کنید")]
        public string EmailAddress { get; set; }

        [Display(Name = "متن پیام")]
        [Required(ErrorMessage = "حرفی برای گفتن ندارید؟")]
        public string Description { get; set; }

        [Required(ErrorMessage = "لطفا {0} را وارد کنید")]
        [Display(Name = "حاصل جمع")]
        public string Captcha { get; set; }
    }

SubscriberVM .cs

    public class SubscriberVM
    {   
        /*[RegularExpression("^[a-zA-Z0-9_\\.-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "آدرس ایمیل صحیح نیست")]*/
          [EmailAddress(ErrorMessage = "آدرس ایمیل صحیح نیست")] /*.Net4.5*/
        [Display(Name = "ایمیل")]
        [Required(ErrorMessage = "لطفا {0} را وارد کنید")]
        public string Email { get; set; }

        [Display(Name = "وضعیت")]
        public bool IsActive { get; set; }    
    }

در Layout، دو اکشن متد صدا زده شده‌اند که وظیفه ارسال ویوهای هر کدام به Layout را به عهده دارند :

        <div class="row footerclass">
            <div class="col-md--6">
                @Html.Action("Subscribers", "Home")
            </div>
            <div class="col-md-6">
                @Html.Action("Contact", "Home")
            </div>

        </div>

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

public ActionResult Contact()
        {
            return PartialView("_Contact", model);
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]

        public ActionResult Contact(ContactVM model)
        {
              if (ModelState.IsValid)
                {
//Do Something                    
                }
            return PartialView("_Contact", model);
        }

        public ActionResult Subscribers()
        {
            return PartialView("_Subscribers");
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Subscribers(SubscriberVM model)
        {
                if (ModelState.IsValid)
                {
//Do Something
                }
            }
            return PartialView("_Subscribers",model);
        }

و اما ویوهایی که قرار است نمایش داده شوند:

Contact.Cshtml

@model IrsaShop.Models.ViewModel.ContactVM


<span></span><span>تماس با ما</span>
<hr />

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)
    <div>
        @Html.TextBoxFor(m => m.Name, new { @class = "form-control", @id = "name", @name = "name", placeholder = "نام" })
        @Html.ValidationMessageFor(m => m.Name)
    </div>
    <div>
        @Html.TextBoxFor(m => m.EmailAddress, new { @class = "form-control", @id = "email", @name = "email", placeholder = "ایمیل", @style = "direction: ltr" })
        @Html.ValidationMessageFor(m => m.EmailAddress)
    </div>
    <div>
        @Html.TextAreaFor(model => model.Description, new { @class = "form-control", @id = "message", @name = "message", placeholder = "پیام", @style = "max-width: 100%;height: 90px;" })
    </div>
    <div>
        <input type="button" value="" id="refresh" />
        <img alt="Captcha" id="imgcpatcha" src="@Url.Action("CaptchaImage","Captcha")" />
    </div>
    <div>
        @Html.TextBoxFor(model => model.Captcha, new { @class = "form-control", placeholder = "حاصل جمع؟" })
        @Html.ValidationMessageFor(model => model.Captcha)

    </div>
    <div>
        <input type="submit" value="ارسال" name="submitValue" />
    </div>
}

_Subscriber.Csh tml 

@model IrsaShop.Models.SubscriberVM

<span></span><span>خبرنامه</span>
<hr/>

@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
    @Html.ValidationSummary(true)
    <div>

        <div>
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control right-buffer top-buffer pull-right", @id = "email", @name = "email", placeholder = "ایمیل", @style = "direction: ltr;width: 50%", @required = "required" })
       @*     <button type="submit" name="submitValue">ثبت ایمیل</button>*@
            <input type="submit" value="ثبت ایمیل" name="submitValue" />
        </div>
        
    </div>
   @Html.ValidationMessageFor(m => m.Email,"",new { @class = "right-buffer pull-right"})
   
}

نکته اول : هیچ نوع ورودی برای Html.BeginForm در نظر گرفته نشده است. اگر اکشن متدی را برای صدا زدن در این بخش در نظر بگیرید، هنگام Postback به مشکل برخورد خواهید کرد؛ چون آدرس آن اکشن متد به شکل صریح در آدرس مرورگر فراخوانی میشود و پارشال ما پس از Post-back به تنهایی و بدون Layout نمایش داده خواهد شد. اسم بردن از اکشن متد وقتی کارساز است که آن اکشن متد قرار باشد یک Redirect انجام دهد ولی هدف ما این است که صفحه را از دست ندهیم و پیام‌های خطای ModelState را در همان صفحه قبل و پس از Post-back ببینیم و همچنین پس از انجام عملیات (مثلا ارسال پیام) همین صفحه نمایش داده شود. 

نکته دوم : نکته اول یک مشکل دارد! اگر به شکل صریح اکشن متد مربوط به Post-back مشخص نشود، بطور اتوماتیک تمامی اکشن متدهایی که ویژگی [HttpPost] دارند اجرا خواهند شد. این یعنی هر دو اکشن متد Contact و Subscriber اجرا می‌شوند و بنابر آنچه در اکشن متدها نوشته‌ایم هر دو ModelState بررسی می‌شود که این هدف ما نیست. مثلا فرم سمت چپ را تکمیل کرده ایم و دکمه "ثبت ایمیل" را فشار داده‌ایم و صفحه Postback می‌شود و با اینکه ایمیل در بانک ثبت شده اما فرم سمت راستی با خطا ظاهر میشود که چرا فیلدها خالی هستند!؟ 

برای حل این مشکل کافیست خاصیت name مربوط به دکمه‌ها را به شکل یک ورودی برای اکشن متدها بفرستیم و بر اساس وضعیت آن تنها state مدل مورد نظر خودمان را بررسی کنیم. پس اصلاح زیر را برای اکشن متدهای دارای ویژگی [HttpPost] انجام میدهیم.

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]

        public ActionResult Contact(ContactVM model, , string submitValue)
        {
   if (submitValue == "ارسال") 
                {
                 if (ModelState.IsValid)
                {
//Do Something                    
                }
}   else
                {
                         ModelState.Clear();
                }        
            return PartialView("_Subscribers", model);
        }

        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
public ActionResult Subscribers(SubscriberVM model, string submitValue)
        {
             if (submitValue == "ثبت ایمیل") 
            {
if (ModelState.IsValid)
                {
//Do Something
                }
}
            else
            {
                ModelState.Clear();
            }
            return PartialView("_Subscribers");
        }

نکته سوم : در این روش سعی کنید از ViewModel  استفاده کنید و سعی کنید ویو مدل‌ها پراپرتی‌های با نام یکسان نداشته باشند. مثلا پراپرتی Email  در ویو مدل‌ها نام‌های متفاوتی داشته باشند (مثل EmailAddress  ، Email  ، ContactMail  و ...). با اینکار در زمان Postback  احتمال اینکه فیلدهای مشترک اتوماتیک پر شده به ما نمایش داده باشند صفر خواهد شد.

نکته چهارم : حواستان باشد پس از انجام عملیات مرتبط با هر فرم در اکشن متد مربوط به آن (مثلا ارسال ایمیل، ثبت در بانک یا ...) در صورتی که عملیات با موفقیت انجام شد حتما ModelState  را clear کنید. با اینکار پس از Post-back  فیلدهای پارشال‌ها خالی میشوند.

نکته پنجم : میتوانید به سادگی مدیریت خطا را به کمک جی کوئری انجام دهید؛ مثلا فرض کنید میخواهیم اگر ایمیل کاربر برای دریافت خبرنامه با موفقیت ثبت شد، پیامی مبنی بر موفقیت برای وی بفرستیم؛ اکشن متد HttPost مربوط به  Subscriber  را به شکل زیر تکمیل میکنیم : 

[HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public ActionResult Subscribers(SubscriberVM model, string submitValue)
        {
            if (submitValue == "ثبت ایمیل")
            {                
                if (ModelState.IsValid)
                {
                    Subscriber mail = new Subscriber() { Email = model.EmailSubscriber, IsActive = true };
                    context.Subscribers.Add(mail);
                    context.SaveChanges();
                    ViewBag.info = "ایمیل شما با موفقیت ثبت شد.";
                    ViewBag.color = "alert-success";
                    ModelState.Clear();
                }
            }
            else
            {
                ModelState.Clear();
            }
            
            return PartialView("_Subscribers ");
        }

در انتهای پارشال _Subscriber هم چند خط کد زیر را مینویسیم :

@if (!String.IsNullOrEmpty(ViewBag.info))
{
    <div id="info" style="position: fixed; bottom: 0; right: 0; margin-right: 1%;">

        <div class="alert @ViewBag.color alert-dismissable">
            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
            <strong> @ViewBag.info</strong>
        </div>

    </div>
    <script>
        $(function () {
            $("#info").fadeOut(15000);
        });
    </script>
}


نتیجه این خواهد بود که پس از PostBack در صورت موفقیت تصویر زیر را خواهیم دید و 15 ثانیه المان سبزرنگ بوت استرپِ زیر نمایش داده خواهد شد.

این روش نوعی مدیریت میان اکشن متدهای دارای ویژگی HttpPost است و همانطور که گفتیم به علت اینکه پس از Post-Back نیاز به ساختار به هم نخورده‌ی صفحه‌ی قبلی داریم، نمیتوانستیم به شکل صریح، اکشن متد برای Html.BeginForm تعریف کنیم تا این دردسر‌ها را نداشته باشیم.

حین نوشتن این مقاله به علت وجود if ‌های تو در تو، امیدوار بودم که روش‌های بهتری برای اینکار موجود باشند و هنوز هم امیدوارم نظرات شما چنین چیزی را نشان دهد. 

مطالب
Globalization در ASP.NET MVC - قسمت هفتم
در قسمت قبل مطالب تکمیلی تولید پرووایدر سفارشی منابع دیتابیسی ارائه شد. در این قسمت نحوه بروزرسانی ورودی‌های منابع در زمان اجرا بحث می‌شود.

.

تولید یک پرووایدر منابع دیتابیسی - بخش سوم

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

مثلا برای پرووایدر سفارشی دیتابیسی تهیه‌شده در مطالب قبلی، تنها کافی است ابزاری تهیه شود تا به کاربران اجازه به‌روزرسانی مقادیر موردنظرشان در دیتابیس را بدهد که کاری بسیار ساده است. بدین ترتیب به‌روزرسانی این مقادیر در زمان اجرا کاری بسیار ابتدایی به نظر می‌رسد. اما در قسمت قبل نشان داده شد که برای بالا بردن بازدهی بهتر است که مقادیر موجود در دیتابیس در حافظه سرور کش شوند. استراتژی اولیه و ساده‌ای نیز برای نحوه پیاده‌سازی این فرایند کشینگ ارائه شد. بنابراین باید امکاناتی فراهم شود تا درصورت تغییر مقادیر کش‌شده در سمت دیتابیس، برنامه از این تغییرات آگاه شده و نسبت به به‌روزرسانی این مقادیر در متغیر کشینگ اقدامات لازم را انجام دهد.

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

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


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


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


پیاده‌سازی امکان پاک‌سازی مقادیر کش‌شده

برای این‌کار باید تغییراتی در کلاس DbResourceManager داده شود تا بتوان این کلاس را از تغییرات بوجود آمده آگاه ساخت. روشی که من برای این کار درنظر گرفتم استفاده از یک اینترفیس حاوی اعضای موردنیاز برای پیاده‌سازی این امکان است تا مدیریت این ویژگی در ادامه راحت‌تر شود.


اینترفیس IDbCachedResourceManager

این اینترفیس به صورت زیر تعریف شده است:

namespace DbResourceProvider
{
  internal interface IDbCachedResourceManager
  {
    string ResourceName { get; }

    void ClearAll();
    void Clear(string culture);
    void Clear(string culture, string resourceKey);
  }
}

در پراپرتی فقط خواندنی ResourceName نام منبع کش شده ذخیره خواهد شد.

متد ClearAll برای پاک‌سازی تمامی ورودی‌های کش‌شده استفاده می‌شود.

متدهای Clear برای پاک‌سازی ورودی‌های کش‌شده یک کالچر به خصوص و یا یک ورودی خاص استفاده می‌شود.

با استفاده از این اینترفیس، پیاده‌سازی کلاس DbResourceManager به صورت زیر تغییر می‌کند:

using System.Collections.Generic;
using System.Globalization;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  internal class DbResourceManager : IDbCachedResourceManager
  {
    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) { ... }
    private object GetCachedObject(string resourceKey, string cultureName) { ... }

    #region Implementation of IDbCachedResourceManager
    public string ResourceName
    {
      get { return _resourceName; }
    }
    public void ClearAll()
    {
      lock (this)
      {
        _resourceCacheByCulture.Clear(); 
      }
    }
    public void Clear(string culture)
    {
      lock (this)
      {
        if (!_resourceCacheByCulture.ContainsKey(culture)) return;
        _resourceCacheByCulture[culture].Clear(); 
      }
    }
    public void Clear(string culture, string resourceKey)
    {
      lock (this)
      {
        if (!_resourceCacheByCulture.ContainsKey(culture)) return;
        _resourceCacheByCulture[culture].Remove(resourceKey); 
      }
    }
    #endregion
  }
}

اعضای اینترفیس IDbCachedResourceManager به صورت مناسبی در کد بالا پیاده‌سازی شدند. در تمام این پیاده‌سازی‌ها مقادیر مربوطه از درون متغیر کشینگ پاک می‌شوند تا پس از اولین درخواست، بلافاصله از دیتابیس خوانده شوند. برای جلوگیری از دسترسی هم‌زمان نیز از بلاک lock استفاده شده است.

برای استفاده از این امکانات جدید همان‌طور که در بالا نیز اشاره شد باید بتوان نمونه‌های تولیدی از کلاس DbResourceManager توسط ASP.NET درون متغیری استاتیک ذخیره شوند. برای اینکار از کلاس جدیدی با عنوان DbResourceCacheManager استفاده می‌شود که برخلاف تمام کلاس‌های تعریف‌شده تا اینجا با سطح دسترسی public تعریف می‌شود.


کلاس DbResourceCacheManager

مدیریت نمونه‌های تولیدی از کلاس DbResourceManager در این کلاس انجام می‌شود. این کلاس پیاده‌سازی ساده‌ای به‌صورت زیر دارد:

using System.Collections.Generic;
using System.Linq;
namespace DbResourceProvider
{
  public static class DbResourceCacheManager
  {
    internal static List<IDbCachedResourceManager> ResourceManagers { get; private set; }
    static DbResourceCacheManager()
    {
      ResourceManagers = new List<IDbCachedResourceManager>();
    }
    public static void ClearAll()
    {
      ResourceManagers.ForEach(r => r.ClearAll());
    }
    public static void Clear(string resourceName)
    {
      GetResouceManagers(resourceName).ForEach(r => r.ClearAll());
    }
    public static void Clear(string resourceName, string culture)
    {
      GetResouceManagers(resourceName).ForEach(r => r.Clear(culture));
    }
    public static void Clear(string resourceName, string culture, string resourceKey)
    {
      GetResouceManagers(resourceName).ForEach(r => r.Clear(culture, resourceKey));
    }

    private static List<IDbCachedResourceManager> GetResouceManagers(string resourceName)
    {
      return ResourceManagers.Where(r => r.ResourceName.ToLower() == resourceName.ToLower()).ToList();
    }
  }
}

ازآنجاکه نیازی به تولید نمونه ای از این کلاس وجود ندارد، این کلاس به صورت استاتیک تعریف شده است. بنابراین تمام اعضای درون آن نیز استاتیک هستند.

از پراپرتی ResourceManagers برای نگهداری لیستی از نمونه‌های تولیدی از کلاس DbResourceManager استفاده می‌شود. این پراپرتی از نوع <List<IDbCachedResourceManager تعریف شده است و برای جلوگیری از دسترسی بیرونی، سطح دسترسی آن internal درنظر گرفته شده است.

در کانستراکتور استاتیک این کلاس (اطلاعات بیشتر درباره static constructor در اینجا) این پراپرتی با مقداردهی به یک نمونه تازه از لیست، اصطلاحا initialize می‌شود.

سایر متدها نیز برای فراخوانی متدهای موجود در اینترفیس IDbCachedResourceManager پیاده‌سازی شده‌اند. تمامی این متدها دارای سطح دسترسی public هستند. همان‌طور که می‌بینید از خاصیت ResourceName برای مشخص‌کردن نمونه موردنظر استفاده شده است که دلیل آن در قسمت قبل شرح داده شده است.

دقت کنید که برای اطمینان از انتخاب درست همه موارد موجود در شرط انتخاب نمونه موردنظر در متد GetResouceManagers از متد ToLower برای هر دو سمت شرط استفاده شده است.


نکته مهم: درباره علت برگشت یک لیست از متد انتخاب نمونه موردنظر از کلاس DbResourceManager در کد بالا (یعنی متد GetResouceManagers) باید نکته‌ای اشاره شود. در قسمت قبل عنوان شد که سیستم مدیریت منابع ASP.NET نمونه‌های تولیدی از پرووایدرهای منابع را به ازای هر منبع کش می‌کند. اما یک نکته بسیار مهم که باید به آن توجه کرد این است که این کش برای «عبارات بومی‌سازی ضمنی» و نیز «متد مربوط به منابع محلی» موجود در کلاس HttpContext و یا نمونه مشابه آن در کلاس TemplateControl (همان متد GetLocalResourceObject که درباره این متدها در قسمت سوم این سری شرح داده شده است) از یکدیگر جدا هستند و استفاده از هریک از این دو روش موجب تولید یک نمونه مجزا از پرووایدر مربوطه می‌شود که متاسفانه کنترل آن از دست برنامه نویس خارج است. دقت کنید که این اتفاق برای منابع کلی رخ نمی‌دهد.

بنابراین برای پاک کردن مناسب ورودی‌های کش‌شده در کلاس فوق به جای استفاده از متد Single در انتخاب نمونه موردنظر از کلاس DbResourceManager (در متد GetResouceManagers) از متد Where استفاده شده و یک لیست برگشت داده می‌شود. چون با توجه به توضیح بالا امکان وجود دو نمونه DbResourceManager از یک منبع درخواستی محلی در لیست نمونه‌های نگهداری شده در این کلاس وجود دارد.

.

افزودن نمونه‌ها به کلاس DbResourceCacheManager

برای نگهداری نمونه‌های تولید شده از DbResourceManager، باید در یک قسمت مناسب این نمونه‌ها را به لیست مربوطه در کلاس DbResourceCacheManager اضافه کرد. بهترین مکان برای انجام این عمل در کلاس پایه BaseDbResourceProvider است که درخواست تولید نمونه را در متد EnsureResourceManager درصورت نال بودن آن می‌دهد. بنابراین این متد را به صورت زیر تغییر می‌دهیم:

private void EnsureResourceManager()
{
  if (_resourceManager != null) return;
  {
    _resourceManager = CreateResourceManager();
    DbResourceCacheManager.ResourceManagers.Add(_resourceManager);
  }
}

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

استفاده از کلاس DbResourceCacheManager

پس از پیاده‌سازی تمامی موارد لازم، حالتی را درنظر بگیرید که مقادیر ورودی‌های تعریف شده در منبع "dir1/page1.aspx" تغییر کرده است. بنابراین برای بروزرسانی مقادیر کش‌شده کافی است تا از کدی مثل کد زیر استفاده شود:

DbResourceCacheManager.Clear("dir1/page1.aspx");

کد بالا کل ورودی‌های کش‌شده برای منبع "dir1/page1.aspx" را پاک می‌کند. برای پاک کردن کالچر یا یک ورودی خاص نیز می‌توان از کدهایی مشابه زیر استفاده کرد:

DbResourceCacheManager.Clear("Default.aspx", "en-US");
DbResourceCacheManager.Clear("GlobalTexts", "en-US", "Yes");

.

دریافت کد پروژه

کد کامل پروژه DbResourceProvider به همراه مثال و اسکریپت‌های دیتابیسی مربوطه از لینک زیر قابل دریافت است:

DbResourceProvider.rar

برای استفاده از این مثال ابتدا باید کتابخانه Entity Framework (با نام EntityFramework.dll) را مثلا از طریق نوگت دریافت کنید. نسخه‌ای که من در این مثال استفاده کردم نسخه 4.4 با حجم حدود 1 مگابایت است.

نکته: در این کد یک بهبود جزئی اما مهم در کلاس ResourceData اعمال شده است. در قسمت سوم این سری، اشاره شد که نام ورودی‌های منابع Case Sensitive نیست. بنابراین برای پیاده‌سازی این ویژگی، متدهای این کلاس باید به صورت زیر تغییر کنند:

public Resource GetResource(string resourceKey, string culture)
{
  using (var data = new TestContext())
  {
    return data.Resources.SingleOrDefault(r => r.Name.ToLower() == _resourceName.ToLower() && r.Key.ToLower() == resourceKey.ToLower() && r.Culture == culture);
  }
}

public List<Resource> GetResources(string culture)
{
  using (var data = new TestContext())
  {
    return data.Resources.Where(r => r.Name.ToLower() == _resourceName.ToLower() && r.Culture == culture).ToList();
  }
}
تغییرات اعمال شده همان استفاده از متد ToLower در دو طرف شرط مربوط به نام منابع و کلید ورودی‌هاست.


در آینده...

در ادامه مطالب، بحث تهیه پرووایدر سفارشی فایلهای resx. برای پیاده‌سازی امکان به‌روزرسانی در زمان اجرا ارائه خواهد شد. بعد از پایان تهیه این پرووایدر سفارشی، این سری مطالب با ارائه نکات استفاده از این پرووایدرها در ASP.NET MVC پایان خواهد یافت.


منابع

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

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

مطالب دوره‌ها
استفاده از AOP Interceptors برای حذف کدهای تکراری کش کردن اطلاعات در لایه سرویس برنامه
اکثر برنامه‌های ما دارای قابلیت‌هایی هستند که با موضوعاتی مانند امنیت، کش کردن اطلاعات، مدیریت استثناها، ثبت وقایع و غیره گره خورده‌اند. به هر یک از این موضوعات یک Aspect یا cross-cutting concern نیز گفته می‌شود.
در این قسمت قصد داریم اطلاعات بازگشتی از لایه سرویس برنامه را کش کنیم؛ اما نمی‌خواهیم مدام کدهای مرتبط با کش کردن اطلاعات را در مکان‌های مختلف لایه سرویس پراکنده کنیم. می‌خواهیم یک ویژگی یا Attribute سفارشی را تهیه کرده (مثلا به نام CacheMethod) و به متد یا متدهایی خاص اعمال کنیم. سپس برنامه، در زمان اجرا، بر اساس این ویژگی‌ها، خروجی‌های متدهای تزئین شده با ویژگی CacheMethod را کش کند.
در اینجا نیز از ترکیب StructureMap و DynamicProxy پروژه Castle، برای رسیدن به این مقصود استفاده خواهیم کرد. به کمک StructureMap می‌توان در زمان وهله سازی کلاس‌ها، آن‌ها را به کمک متدی به نام EnrichWith توسط یک محصور کننده دلخواه، مزین یا غنی سازی کرد. این مزین کننده را جهت دخالت در فراخوانی‌های متدها، یک DynamicProxy درنظر می‌گیریم. با پیاده سازی اینترفیس IInterceptor کتابخانه DynamicProxy مورد استفاده و تحت کنترل قرار دادن نحوه و زمان فراخوانی متدهای لایه سرویس، یکی از کارهایی را که می‌توان انجام داد، کش کردن نتایج است که در ادامه به جزئیات آن خواهیم پرداخت.


پیشنیازها

ابتدا یک برنامه جدید کنسول را آغاز کنید. تنظیمات آن‌را از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت‌های قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
 PM> Install-Package structuremap
PM> Install-Package Castle.Core
همچنین ارجاعی را به اسمبلی استاندارد System.Web.dll نیز اضافه نمائید.
از این جهت که از HttpRuntime.Cache قصد داریم استفاده کنیم. HttpRuntime.Cache در برنامه‌های کنسول نیز کار می‌کند. در این حالت از حافظه سیستم استفاده خواهد کرد و در پروژه‌های وب از کش IIS بهره می‌برد.


ویژگی CacheMethod مورد استفاده

using System;

namespace AOP02.Core
{
    [AttributeUsage(AttributeTargets.Method)]
    public class CacheMethodAttribute : Attribute
    {
        public CacheMethodAttribute()
        {
            // مقدار پیش فرض
            SecondsToCache = 10;
        }

        public double SecondsToCache { get; set; }
    }
}
همانطور که عنوان شد، قصد داریم متدهای مورد نظر را توسط یک ویژگی سفارشی، مزین سازیم تا تنها این موارد توسط AOP Interceptor مورد استفاده پردازش شوند.
در ویژگی CacheMethod، خاصیت SecondsToCache بیانگر مدت زمان کش شدن نتیجه متد خواهد بود.


ساختار لایه سرویس برنامه

using System;
using System.Threading;
using AOP02.Core;

namespace AOP02.Services
{
    public interface IMyService
    {
        string GetLongRunningResult(string input);
    }

    public class MyService : IMyService
    {
        [CacheMethod(SecondsToCache = 60)]
        public string GetLongRunningResult(string input)
        {
            Thread.Sleep(5000); // simulate a long running process
            return string.Format("Result of '{0}' returned at {1}", input, DateTime.Now);
        }
    }
}
اینترفیس IMyService و پیاده سازی نمونه آن‌را در اینجا مشاهده می‌کنید. از این لایه در برنامه استفاده شده و قصد داریم نتیجه بازگشت داده شده توسط متدی زمانبر را در اینجا توسط AOP Interceptors کش کنیم.


تدارک یک CacheInterceptor

using System;
using System.Web;
using Castle.DynamicProxy;

namespace AOP02.Core
{
    public class CacheInterceptor : IInterceptor
    {
        private static object lockObject = new object();

        public void Intercept(IInvocation invocation)
        {
            cacheMethod(invocation);
        }

        private static void cacheMethod(IInvocation invocation)
        {
            var cacheMethodAttribute = getCacheMethodAttribute(invocation);
            if (cacheMethodAttribute == null)
            {
                // متد جاری توسط ویژگی کش شدن مزین نشده است
                // بنابراین آن‌را اجرا کرده و کار را خاتمه می‌دهیم
                invocation.Proceed();
                return;
            }

            // دراینجا مدت زمان کش شدن متد از ویژگی کش دریافت می‌شود
            var cacheDuration = ((CacheMethodAttribute)cacheMethodAttribute).SecondsToCache;

            // برای ذخیره سازی اطلاعات در کش نیاز است یک کلید منحصربفرد را
            //  بر اساس نام متد و پارامترهای ارسالی به آن تهیه کنیم
            var cacheKey = getCacheKey(invocation);

            var cache = HttpRuntime.Cache;
            var cachedResult = cache.Get(cacheKey);


            if (cachedResult != null)
            {
                // اگر نتیجه بر اساس کلید تشکیل شده در کش موجود بود
                // همان را بازگشت می‌دهیم
                invocation.ReturnValue = cachedResult;
            }
            else
            {
                lock (lockObject)
                {
                    // در غیر اینصورت ابتدا متد را اجرا کرده
                    invocation.Proceed();
                    if (invocation.ReturnValue == null)
                        return;

                    // سپس نتیجه آن‌را کش می‌کنیم
                    cache.Insert(key: cacheKey,
                                 value: invocation.ReturnValue,
                                 dependencies: null,
                                 absoluteExpiration: DateTime.Now.AddSeconds(cacheDuration),
                                 slidingExpiration: TimeSpan.Zero);
                }
            }
        }

        private static Attribute getCacheMethodAttribute(IInvocation invocation)
        {
            var methodInfo = invocation.MethodInvocationTarget;
            if (methodInfo == null)
            {
                methodInfo = invocation.Method;
            }
            return Attribute.GetCustomAttribute(methodInfo, typeof(CacheMethodAttribute), true);
        }

        private static string getCacheKey(IInvocation invocation)
        {
            var cacheKey = invocation.Method.Name;

            foreach (var argument in invocation.Arguments)
            {
                cacheKey += ":" + argument;
            }

            // todo: بهتر است هش این کلید طولانی بازگشت داده شود
            // کار کردن با هش سریعتر خواهد بود
            return cacheKey;
        }
    }
}
کدهای CacheInterceptor مورد استفاده را در بالا مشاهده می‌کنید.
توضیحات ریز قسمت‌های مختلف آن به صورت کامنت، جهت درک بهتر عملیات، ذکر شده‌اند.


اتصال Interceptor به سیستم

خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شده‌اند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آن‌ها مطلع کنیم.
using System;
using AOP02.Core;
using AOP02.Services;
using Castle.DynamicProxy;
using StructureMap;

namespace AOP02
{
    class Program
    {
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.For<IMyService>()
                 .EnrichAllWith(myTypeInterface =>
                        dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new CacheInterceptor()))
                 .Use<MyService>();
            });

            var myService = ObjectFactory.GetInstance<IMyService>();
            Console.WriteLine(myService.GetLongRunningResult("Test"));
            Console.WriteLine(myService.GetLongRunningResult("Test"));
        }
    }
}
در قسمت تنظیمات اولیه DI مورد استفاده، هر زمان که شیءایی از نوع IMyService درخواست شود، کلاس MyService وهله سازی شده و سپس توسط CacheInterceptor محصور می‌گردد. اکنون ادامه برنامه با این شیء محصور شده کار می‌کند.
حال اگر برنامه را اجرا کنید یک چنین خروجی قابل مشاهده خواهد بود:
 Result of 'Test' returned at 2013/04/09 07:19:43
Result of 'Test' returned at 2013/04/09 07:19:43
همانطور که ملاحظه می‌کنید هر دو فراخوانی یک زمان را بازگشت داده‌اند که بیانگر کش شدن اطلاعات اولی و خوانده شدن اطلاعات فراخوانی دوم از کش می‌باشد (با توجه به یکی بودن پارامترهای هر دو فراخوانی).

از این پیاده سازی می‌شود به عنوان کش سطح دوم ORMها نیز استفاده کرد (صرفنظر از نوع ORM در حال استفاده).

دریافت مثال کامل این قسمت
AOP02.zip
مطالب
درخت‌ها و گراف‌ها قسمت دوم
در قسمت قبلی ما به بررسی درخت و اصطلاحات فنی آن پرداختیم و اینکه چگونه یک درخت را پیمایش کنیم. در این قسمت مطلب قبل را با درخت‌های دودویی ادامه می‌دهیم.

درخت‌های دودویی Binary Trees
همه‌ی موضوعات و اصطلاحاتی را که در مورد درخت‌ها به کار بردیم، در مورد این درخت هم صدق می‌کند؛ تفاوت درخت دودویی با یک درخت معمولی این است که درجه هر گره نهایتا دو خواهد بود یا به عبارتی ضریب انشعاب این درخت 2 است. از آن جایی که هر گره در نهایت دو فرزند دارد، می‌توانیم فرزندانش را به صورت فرزند چپ Left Child و فرزند راست Right Child صدا بزنیم. به گره‌هایی که فرزند ریشه هستند اینگونه می‌گوییم که گره فرزند چپ با همه فرزندانش می‌شوند زیر درخت چپ Left SubTree و گره سمت راست ریشه با تمام فرزندانش زیر درخت راست Right SubTree صدا زده می‌شوند.

نحوه پیمایش درخت دودویی

این درخت پیمایش‌های گوناگونی دارد ولی سه تای آن‌ها اصلی‌تر و مهمتر هستند:

In-order یا LVR (چپ، ریشه، راست): در این حالت ابتدا گره‌های سمت چپ ملاقات (چاپ) می‌شوند و سپس ریشه و بعد گره‌های سمت راست.

Pre-Order یا VLR (ریشه، چپ، راست) : در این حالت ابتدا گره‌های ریشه ملاقات می‌شوند. بعد گره‌های سمت چپ و بعد گره‌های سمت راست.

Post_Order یا LRV (چپ، راست، ریشه ): در این حالت ابتدا گره‌های سمت چپ، بعد راست و نهایتا ریشه، ملاقات می‌شوند.

حتما متوجه شده‌اید که منظور از v در اینجا ریشه است و با تغییر و جابجایی مکان این سه حرف RLV میتوانید به ترکیب‌های مختلفی از پیمایش دست پیدا کنید.

اجازه دهید روی شکل بالا پیمایش LVR را انجام دهیم: همانطور که گفتیم باید اول گره‌های سمت چپ را خواند، پس از 17 به سمت 9 حرکت می‌کنیم و می‌بینیم که 9، خود والد است. پس به سمت 6 حرکت می‌کنیم و می‌بینیم که فرزند چپی ندارد؛ پس خود 6 را ملاقات می‌کنیم، سپس فرزند راست را هم بررسی می‌کنیم که فرزند راستی ندارد پس کار ما اینجا تمام است و به سمت بالا حرکت می‌کنیم. 9 را ملاقات می‌کنیم و بعد عدد 5 را و به 17 بر می‌گردیم. 17 را ملاقات کرده و سپس به سمت 15 می‌رویم و الی آخر ...

6-9-5-17-8-15-10

VLR:

17-9-6-5-15-8-10

LRV:

6-5-9-8-10-15-17


نحوه پیاده سازی درخت دودویی:

public class BinaryTree<T>
{
    /// <summary>مقدار داخل گره</summary>
    public T Value { get; set; }
 
    /// <summary>فرزند چپ گره</summary>
    public BinaryTree<T> LeftChild { get; private set; }
 
    /// <summary>فرزند راست گره</summary>
    public BinaryTree<T> RightChild { get; private set; }
   
    /// <summary>سازنده کلاس</summary>
    /// <param name="value">مقدار گره</param>
    /// <param name="leftChild">فرزند چپ</param>
    /// <param name="rightChild">فرزند راست
    /// </param>
    public BinaryTree(T value,
        BinaryTree<T> leftChild, BinaryTree<T> rightChild)
    {
        this.Value = value;
        this.LeftChild = leftChild;
        this.RightChild = rightChild;
    }
 
    /// <summary>سازنده بدون فرزند
    /// </summary>
    /// <param name="value">the value of the tree node</param>
    public BinaryTree(T value) : this(value, null, null)
    {
    }
 
    /// <summary>‏‏‎LVR پیمایش</summary>
    public void PrintInOrder()
    {
        // ملاقات فرزندان زیر درخت چپ
        if (this.LeftChild != null)
        {
            this.LeftChild.PrintInOrder();
        }
 
        // ملاقات خود ریشه
        Console.Write(this.Value + " ");
 
        // ملاقات فرزندان زیر درخت راست
        if (this.RightChild != null)
        {
            this.RightChild.PrintInOrder();
        }
    }
}
 
/// <summary>
/// نحوه استفاده از کلاس بالا
/// </summary>
public class BinaryTreeExample
{
    static void Main()
    {
        BinaryTree<int> binaryTree =
            new BinaryTree<int>(14,
                    new BinaryTree<int>(19,
                          new BinaryTree<int>(23),
                          new BinaryTree<int>(6,
                                  new BinaryTree<int>(10),
                                  new BinaryTree<int>(21))),
                    new BinaryTree<int>(15,
                          new BinaryTree<int>(3),
                          null));
 
        binaryTree.PrintInOrder();
        Console.WriteLine();
 
        // خروجی
        // 23 19 10 6 21 14 3 15
    }
}

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

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

  // ملاقات فرزندان زیر درخت چپ
        if (this.LeftChild != null)
        {
            this.LeftChild.PrintInOrder();
        }
 
        // ملاقات خود ریشه
        Console.Write(this.Value + " ");
 
        // ملاقات فرزندان زیر درخت راست
        if (this.RightChild != null)
        {
            this.RightChild.PrintInOrder();
        }


درخت دودویی مرتب شده Ordered Binary Search Tree

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

همان طور که می‌دانید برای مقایسه اعداد ما از علامتهای <>= استفاده می‌کنیم و اعداد صحیح بهترین اعداد برای مقایسه هستند. در درخت‌های جست و جوی دو دویی یک خصوصیت اضافه به اسم کلید هویت یکتا Unique identification  Key داریم که یک کلید قابل مقایسه است. در تصویر زیر ما دو گره با مقدارهای متفاوتی داریم که با مقایسه‌ی آنان می‌توانیم کوچک و بزرگ بودن یک گره را محاسبه کنیم. ولی به این نکته دقت داشته باشید که این اعداد داخل دایره‌ها، دیگر برای ما حکم مقدار ندارند و کلید‌های یکتا و شاخص هر گره محسوب می‌شوند.

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

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

برای مثال فکر کنید می‌خواهید عدد 13 را در درخت بالا پیدا کنید. ابتدا گره والد 19 را مقایسه کرده و از آنجا که 19 بزرگتر از 13 است می‌دانیم که 13 را در زیر درخت راست پیدا نمی‌کنیم. پس زیر درخت چپ را مقایسه می‌کنیم (بنابراین به راحتی یک زیر درخت از مقایسه و عمل جست و جو کنار گذاشته شد). سپس گره 11 را مقایسه می‌کنیم و از آنجا که 11 کوچکتر از 13 هست، زیر درخت سمت راست را ادامه می‌دهیم و چون 16 بزرگتر از 13 هست، زیر درخت سمت چپ را در ادامه مقایسه می‌کنیم که به 13 رسیدیم.

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

19-11-16-13

درخت هر چه بزرگتر باشد این روش کارآیی خود را بیشتر نشان می‌دهد.

در قسمت بعدی به پیاده سازی کد این درخت به همراه متدهای افزودن و جست و جو و حذف می‌پردازیم.

مطالب
مقدمه ای بر CQRS و Event Sourcing
به صورت عام، functionality اکثر پروژه‌های نرم افزاری تجاری خلاصه میشود به مخفف معروفی به نام CRUD، که object‌ها را میسازیم، آن‌ها را میخوانیم و تغییر میدهیم.
اپلیکیشن‌های طراحی شده بدین صورت، قابلیت خوانایی بالایی خواهند داشت و دیاگرام طراحی آنها چیزی شبیه به تصویر زیر میباشد

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

زمانیکه نیاز‌های پروژه روز به روز افزورده و پیچیده‌تر میشود، مدل CRUD بصورت پیوسته از ارزشش کاسته میشود و از آن سادگی اولیه‌ی در درک و خوانایی آن دور خواهد شد.

ذات CQRS بر آن است که شما مدل‌های مختلفی را برای خواندن و نوشتن دیتا داشته باشید. الگوی آن چیزی شبیه به تصویر زیر است

چیزی که در این روش مشهود است این میباشد که برنامه نویسان باید قسمت‌های Command و Query را به صورت جداگانه طراحی نمایند.
CQRS این قابلیت را به شما میدهد که interface و Datastore و حتی بطور کامل Technology مجزایی در قسمت‌های CQ داشته باشید.

Event Sourcing
قسمت دوم و متمایز، معماری Event Sourcing یا ES میباشد که بصورت کوتاه، ES یک روش متفاوت برای Data storage میباشد.
اکثرا ما از Datastore‌هایی که مدلی از دیتا را انعکاس می‌دهند استفاده میکنیم. به مثال ساده‌ی زیر توجه کنید

ES از برنامه نویسان میخواهد که مدل سنتی CRUD را فراموش کرده و بجای آن تغییراتی را که روی دیتا صورت گرفته، نیز درج نمایند. اینکار به وسیله‌ی یک دیتابیس Append-only انجام میشود که به نام Event Store شناخته میشود.

در این معماری ما همه‌ی تغییرات روی دیتا را به صورت Serialize Event ذخیره میکنیم که میتواند دوباره در هر زمانی اجرا شده و current state هر objectی را در اختیار بگذارد.

این روش به ما کمک بزرگی میکند تا وضعیت یک object را در گذشته به راحتی پیدا کنیم و از آن میتوان به غیر از فوایدی که دارد، به عنوان یک Logger نیز استفاده نمود. به دلیل اینکه جزء به جزء تغییرات بر روی state سیستم، در آن ثبت شده است. از آنجاییکه دیتا بصورت serialize ذخیره میشود، بارگزاری آن نیز با سرعت بالایی انجام خواهد شد.

پس بصورت خلاصه در معماری Command Query Responsibility Segregation ابتدا باید به این موضوع توجه داشت که قسمت‌های Read و Write نرم افزار به صورت مجزایی طراحی میشوند و Event Sourcing شامل تغییراتی که روی data انجام شده است، میباشد و به‌صورت Serialize شده ذخیره میشود. ما تنها به یک دیتابیس و یک جدول برای نمایش event store نیازمندیم (بستگی به نیازتان میتوان تعداد آن را نیز بیشتر نمود و همچنین حتما لزومی ندارد که از دیتابیس‌های رابطه‌ای استفاده شود؛ بصورت مثال پیاده سازی این قسمت را میتوان با استفاده از Redis که دیتابیسی غیر رابطه‌ای و باسرعت میباشد استفاده نمود).
برای شروع کار (نه پیاده سازی کامل) باید با قسمت‌های مختلف طراحی در این معماری آشنا شویم:
Domain Object
نکته: SimpleCqrs فریم ورکی برای پیاده سازی معماری CQRS , ES میباشد که برای ساده‌تر شدن کار، از آن استفاده شده است (شما حتی میتوانید پیاده سازی خود را داشته باشید)
مدل Movie از کلاسی به نام AggregateRoot ارث بری کرده‌است که توسط SimpleCQRS پیاده سازی شده‌است و یک guid key در آن تعبیه شده است (Aggregate root از مباحث Domain Driven برگرفته شده است و آشنایی با آن کمک شایانی به درک عمیق‌تر روی این مباحث مینماید).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie() { }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        //پیاده سازی خواهد شد
    }
}
توجه: SimpleCQRS فقط پیاده سازی guid برای کلید مربوط به هر مدل را پیاده سازی نموده است؛ بنابراین کلید مدل نمیتواند integer باشد.

Commands
command دستوراتی است که توسط end user فراخوانی میشود که باعث تغییرات خواهد شد. وقتی اپلیکیشن یک command را دریافت مینماید، command handler به پردازش آن برای فهمیدن خواسته کاربر میپردازد و پس از آن event مربوطه را برای اجرای آن وظیفه‌ی خاص صدا میزند.
همه‌ی command‌ها تغییراتی بر روی state جاری خواهند داشت. در نتیجه دیتا‌های ذخیره شده درون دیتابیس تغییرات خواهند کرد. هر commandی که تغییری بر روی State سیستم نداشته باشد، یک دستور غلط محسوب شده و باید در سمت query‌ها آن را پیاده سازی نمود.
در نتیجه Commnad‌ها دستوراتی هستند که از طرف کاربر برای تغییرات بر روی دیتا‌های ذخیره شده، ارسال میشوند.
فرض کنید Domain Objectی برای Movie تعریف کرده‌ایم و میخواهیم دستور اضافه کردن فیلم را پیاده سازی نماییم
public class CreateMovieCommand : ICommand  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }
    public CreateMovieCommand(string title, DateTime releaseDate, int runningTime)
    {
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
توجه: ICommand از طریق SimpleCQRS اضافه شده‌است.

Command Handler
بعد از اینکه Command مورد نیاز نوشته شد، حال احتیاج به پیاده سازی CommandHandler مربوطه که دستور متناظر را پردازش میکند، داریم.
public class CreateMovieCommandHandler : CommandHandler<CreateMovieCommand>  
{
    protected IDomainRepository _repository;

    public CreateMovieCommandHandler(IDomainRepository repository)
    {
        _repository = repository;
    }

    public override void Handle(CreateMovieCommand command)  
    {
        var movie = new Domain.Movie(Guid.NewGuid(), command.Title, 
    command.ReleaseDate, command.RunningTimeMinutes);

        _repository.Save(movie);
    }
}
Command Handler باید از کلاس جنریک <CommandHandler<T ارث بری نماید و T باید از نوع Command در نظر گرفته شود و همچنین IDomainRepository اینترفیسی است که توسط SimpleCQRS تعریف شده‌است و ما احتیاجی به پیاده سازی آن نداریم (در قسمت‌های بعدی پیکربندی آن را انجام میدهیم).
برای رسیدگی کردن به دستور مربوطه احتیاج به override کردن متد Handle میباشد.
کار اساسی توسط متد Save انجام میشود که همه‌ی event‌های pending شده توسط Domain Object را گرفته و آنها را به Event Store میفرستد.

Events
event‌ها تغییراتی هستند بر روی State جاری سیستم که توسط کاربر به وسیله‌ی Commandها فراخوانی میشوند.
رویداد‌ها serialize میشوند و درون Event Store ذخیره میشوند؛ بنابراین میتوان فراخوانی آنها را در هر لحظه انجام داد.
هر تعداد Event میتواند توسط یک دستور raise شود.
ساخت یک Event:
قبلا دستوری را برای ساخت یک movie نوشتیم و حال احتیاج به event مربوطه را داریم:
public class MovieCreatedEvent : DomainEvent  
{
    public Guid MovieId
    {
        get { return AggregateRootId; }
        set { AggregateRootId = value;}
    }

    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public MovieCreatedEvent(Guid movieId, string title, DateTime releaseDate, int runningTime)
    {
        MovieId = movieId;
        Title = title;
        ReleaseDate = releaseDate;
        RunningTimeMinutes = runningTime;
    }
}
فراموش نکنید که این کلاس آبجکتی خواهد بود که Serialize شده و در دیتابیس ذخیره خواهد شد. باید همه‌ی پراپرتی‌های لازم که با استفاده از این Event ممکن است تغییر کنند را شامل شود (بدیهی است که این پراپرتی‌ها از Domain Object گرفته میشود).
public class Movie : AggregateRoot  
{
    public string Title { get; set; }
    public DateTime ReleaseDate { get; set; }
    public int RunningTimeMinutes { get; set; }

    public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes)
    {
        Apply(new MovieCreatedEvent(Guid.NewGuid(), title, releaseDate, runningTimeMinutes));
    }
}
به Aggregate فوق که در اوایل بحث صحبت شده‌است دقت کنید. حال متد Apply باعث میشود که event مربوطه درون بخش لوکال aggregate root ذخیره شود. بنابراین بعدا میتواند به صورت فیزیکی درون Event Store ذخیره شود.

Event Handler
هر Event Handler  میتواند تعداد زیادی از IHandleDomainEvents ‌ها را پیاده سازی نماید. حال متد Handle این اینترفیس را پیاده سازی نمودیم. 
public class MovieEventHandler : IHandleDomainEvents<MovieCreatedEvent>  
{
    public void Handle(MovieCreatedEvent createdEvent)
    {
        using (MoviesContext entities = new MoviesContext())
        {
            entities.Movies.Add(new Movie()
            {
                Id = createdEvent.AggregateRootId,
                Title = createdEvent.Title,
                ReleaseDate = createdEvent.ReleaseDate,
                RunningTimeMinutes = createdEvent.RunningTimeMinutes
            });

            entities.SaveChanges();
        }
    }
}
مثلا در این قسمت با استفاده از ORM، شیء مورد نظر به صورت فیزیکی درون دیتابیس ذخیره میشود.
در قسمت آخر نیازمندیم که تغییرات زیر را به Movie اضافه نماییم.
درون Doamin Objectی که قبلا تعریف کرده بودیم متدی را به صورت زیر پیاده سازی مینماییم
protected void OnMovieCreated(MovieCreatedEvent domainEvent)
        {
            Id = domainEvent.AggregateRootId;
            Title = domainEvent.Title;
            ReleaseDate = domainEvent.ReleaseDate;
            RunningTimeMinutes = domainEvent.RunningTimeMinutes;
        }
باعث میشود پس از فراخوانی شدن Event، تغییرات صورت گرفته‌ی بر state سیستم، بر روی Domain Object اعمال شود و آن را بروزرسانی نماید. این متد دقیقا بصورت اتوماتیک وقتی که event مربوطه raise میشود، فراخوانی میشود.
پس از ترکیب CQRS و ES معماری اولیه‌ی سیستم چیزی شبیه به دیاگرام زیر خواهد بود (بسته به سناریوهای خاص میتواند سفارشی سازی شود)

خلاصه:
کاربر دستوری را از طریق برنامه به سیستم ارسال مینماید.
command مربوطه دریافت میشود و به روی Command Bus قرار داده میشود.
Command Handler وظیفه‌ی تفسیر کردن Command مربوطه را به عهده میگیرد و به وسیله‌ی Domain object آن event مورد نظر فراخوانی خواهد شد و باعث میشود domain object بروزرسانی گردد.
Event همان objectی است که باید به صورت serialize شده درون append only database ذخیره شود.
Event handler رویداد مربوطه را گرفته و بصورت فیزیکی مقادیر مورد نظر را در دیتابیس ذخیره مینماید.

Query
از آنجاییکه قسمت Read، در سیستم به صورت CQRS طراحی میشود، به راحتی میتوان query‌ها را optimize کرده و به صورت مثال به جای استفاده از ORM‌های معمول بطور مستقیم Stored Procedure فراخوانی کرده، تا جای ممکن کیفیت query‌ها بهترین حالت ممکن باشند. در حالیکه در مدل CRUD بهینه کردن بخش read بسیار پیچیده و بعضا غیر ممکن میباشد.

مزایای استفاده از این مدل
Distributed Systems Capabilities
یکی از مهمترین مزیت‌های این مدل تسهیم گسترش پذیری سیستم بر روی ماشین‌های فیزیکی مختلف از طریق messaging pattern میباشد.
High Availability
از آنجایی که سیستم توزیع پذیر طراحی شده‌است، هر قسمت از آن میتواند بدون توجه به fail شدن قسمت‌های دیگر به کار خود ادامه دهد.
Reduce Complexity
در domain‌های پیچیده طراحی و پیاده سازی objectهایی که مسئول دو قسمت read و write هستند، میتواند کار را بیش از حد پیچیده کرده و در این صورت چون business logic و read logic در هم ترکیب میشوند، مدیریت کردن موارد multiple user, shared data, performance, transactions, consistency سخت و سخت‌تر میشود.
Facilitates Building Task-based UI
وقتی شما به پیاده سازی الگوی CQRS میپردازید، اصولا هر عملی که توسط End user از طریق ui ارسال میشود، معادل command مربوط به آن وجود دارد. به همین جهت میتوان عملیات لازم برای اجرای یک پروسه را بصورت واضحی درک کرد.
 Maintenance And Flexibility
هر چند پیاده سازی این مدل سخت خواهد بود، اما در ابعاد وسیع‌تر به دلیل اینکه هر قسمت به صورت مجزایی طراحی شده و اینکه دستورات و رویداد‌ها به صورت تفکیک شده پیاده سازی شده‌اند، همچنین وجود ES، قابلیت زیادی به debug سیستم می‌دهد.
نکته: ES مدل مورد قبولی برای اکثر معماری‌های نوین سیستم‌های نرم افزاری امروزی میباشد و فقط مختص به CQRS نمیباشد. بطور مثال در معماری Microservices به وفور از Event Sourcing استفاده میشود.

مشکلات استفاده از این مدل
  • ذاتا پیاده سازی این مدل سخت و دشوار است و از آنجاییکه سادگی در پیاده سازی سیستم‌های نرم افزاری، یک اصل مهم محسوب میشود، بنابراین استفاده از این مدل محدود میشود به سیستم‌های نرم افزاری که مزیت‌های گفته شده در قسمت فوق برایشان حیاتی محسوب شود.
  • برای پیاده سازی سیستمی با این مدل احتیاج به تیم توسعه‌ای است که با مفاهیم آن کاملا آشنا باشد.
  • هر چند امروزه فضای فیزیکی برای ذخیره سازی دیتا ارزان محسوب میشود، اما به هر حال استفاده از این مدل به همراه ES، حجم زیادی از Disk space را خواهد گرفت.
  • همانطور که دیدید برای پیاده سازی یک Insert ساده، حجم زیادی کد نوشته شده‌است. بنابراین تولید اینگونه نرم افزار‌ها به زمان بیشتری نیاز دارد.
بنابراین باید در انتخاب معماری سیستم بسیار دقت شود؛ هر چند که این مدل برای سیستم‌های بزرگ و پیچیده خیلی کارآمد محسوب میشود و باعث یک Domain object غنی ، History Tracking، شفافیت در مشکلات Concurrency و همچنین Scalability و غیره خواهد شد، اما پیدا کردن برنامه نویسانی با داشتن درک عمیق روی این مباحث کمی سخت به نظر میرسد.
در قسمت بعدی بصورت کامل به پیاده سازی این الگو در یک اپلیکشن دات نتی خواهیم پرداخت.
بازخوردهای دوره
نکته‌ای در مورد مدیریت طول عمر اشیاء در حالت HybridHttpOrThreadLocalScoped در برنامه‌های دسکتاپ
هنگام به روز رسانی اجزاء یک موجودیت چه با کتابخانه ثالث (مثلا GraphDiff) یا روش شخصی خودتان حتما این نکته رو باید در نظر داشته باشید؛ چند ساعتی صرف خطایابی خطای زیر شد که خوشبختانه با این نکته بیان شده در اینجا مشکل حل شد.
An item with the same key has already been added.
نحوه شناسایی خطا هم به این صورت بود که تکه کد زیر در وب بدون خطا اجرا می‌شد ولی در محیط دسک تاپ خطای اشاره شده در بالا صادر می‌شد.
_uow.Update(employee, x => x.OwnedCollection(y => y.RemainingLessons));


بازخوردهای دوره
انتقال خودکار Data Annotations از مدل‌ها به ViewModelهای ASP.NET MVC به کمک AutoMapper
آیا از این روش میشه تمام Data Annotations های مدل رو برای ViewModel فرستاد؟ 
مثلا من توی مدل ام از ویژگی AdditionalMetadata استفاده کردم و توی View هم از کد زیر برای نمایش اطلاعات آنها استفاده میکنم.
@ModelMetadata.FromLambdaExpression(x => x.Name, ViewData).AdditionalValues["HelpTag"]
اما خطای زیر ارسال میشه:
The given key was not present in the dictionary 
نظرات اشتراک‌ها
معرفی کتابخانه‌ی DNTCaptcha.Core
سلام
علت بروز خطای زیر در زمانهای مختلف چیست؟
isValidCookie:: هفت هزار و یکصد و چهل و هشت:�y��ă\�sVvy�S��
Fߌ���銖3? != هفت هزار و یکصد و چهل و هشت
[
  {
    "Key": "{OriginalFormat}",
    "Value": "isValidCookie:: هفت هزار و یکصد و چهل و هشت:�y��ă\\�sVvy�S��\nFߌ���銖\u00053? != هفت هزار و یکصد و چهل و هشت"
  }
]