مطالب
اصول و قراردادهای نام‌گذاری در دات‌نت
نامگذاری (Naming) اشیا یک برنامه شاید در نگاه اول دارای اهمیت بالایی نباشه، اما تجربه نشون داده که در پروژه‌های بزرگ که با کمک چندین مجموعه به انجام میرسه نامگذاری صحیح و اصولی که از یکسری قواعد کلی و مناسب پیروی میکنه میتونه به پیشبرد اهداف و مدیریت راحتتر برنامه کمک بسیاری بکنه.
بیشتر موارد اشاره شده در این مطلب از کتاب جامع و مفید Framework Design Guidelines اقتباس شده که خوندن این کتاب مفید رو به خوانندگان توصیه میکنم.
برای کمک به نوشتن اصولی و راحتتر سورسهای برنامه‌ها در ویژوال استودیو نرم افزارهای متعددی وجود داره که با توجه به تجربه شخصی خودم نرم افزار Resharper  محصول شرکت Jetbrains یکی از بهترین هاست که در مورد خاص مورد بحث در این مطلب نیز بسیار خوب عمل میکنه.
برخی از موارد موجود در مطلب جاری نیز از قراردادهای پیشفرض موجود در نرم افزار Resharper نسخه 6.0 برگرفته شده است و قسمتی نیز از تجربه شخصی خودم و سایر دوستان و همکاران بوده است.
اصل این مطلب حدود یکسال پیش تهیه شده و اگر نقایصی وجود داره لطفا اشاره کنین.

اصول و قراردادهای نام‌گذاری در دات‌نت
انواع نام‌گذاری
نام‌گذاری اشیا در حالت کلی را می‌توان به سه روش زیر انجام داد:
1. Pascal Casing: در این روش حرف اول هر کلمه در نام شی به صورت بزرگ نوشته می‌شود.
FirstName
2. camel Casing: حرف اول در اولین کلمه نام هر شی به صورت کوچک و حرف اول بقیه کلمات به صورت بزرگ نوشته می‌شود.
firstName
3. Hungarian: در این روش برای هر نوع شی موجود یک پیشوند درنظر گرفته می‌شود تا از روی نام شی بتوان به نوع آن پی برد. در ادامه و پس از این پیشوندها سایر کلمات بر اساس روش Pascal Casing نوشته می‌شوند.
strFirstName
lblFirstName
نکته: استفاده از این روش به جز در نام‌گذاری کنترل‌های UI منسوخ شده است.

قراردادهای کلی
1. نباید نام اشیا تنها در بزرگ یا کوچک بودن حروف با هم فرق داشته باشند. به عنوان مثال نباید دو کلاس با نام‌های MyClass و myClass داشته باشیم. هرچند برخی از زبان‌ها case-sensitive هستند اما برخی دیگر نیز چنین قابلیتی ندارند (مثل VB.NET). بنابراین اگر بخواهیم کلاس‌های تولیدی ما در تمام محیط‌ها و زبان‌های برنامه نویسی قابل اجرا باشند باید از این قرارداد پیروی کنیم.
2. تا آنجا که امکان دارد باید از به‌کار بردن مخفف کلمات در نام‌گذاری اشیا دوری کنیم. مثلا به جای استفاده از GetChr باید از GetCharacter استفاده کرد.
البته در برخی موارد که مخفف واژه موردنظر کاربرد گسترده ای دارد می‌توان از عبارت مخفف نیز استفاده کرد. مثل UI به جای UserInterface و یا IO به جای InputOutput.
 
آ. اصول نام‌گذاری فضای نام (namespace)
1. اساس نام‌گذاری فضای نام باید از قاعده زیر پیروی کند:
<Company>.<Technology|Produt|Project>[.<Feature>][.<SubNamespace>]
( < > : اجباری        [ ] : اختیاری )
2. برای نام‌گذاری فضای نام باید از روش Pascal Casing استفاده شود.
3. در هنگام تعریف فضاهای نام به وابستگی آنها توجه داشته باشید. به عنوان مثال اشیای درون یک فضای نام پدر نباید به اشیای درون فضای نام یکی از فرزندانش وابسته باشد. مثلا در فضای نام System نباید اشیایی وجود داشته باشند که به اشیای درون فضای نام System.UI وابسته باشند.
4. سعی کنید از نام‌ها به صورت جمع برای عناوین فضای نام استفاده کنید. مثلا به جای استفاده از Kara.CSS.HQ.Manager.Entity از Kara.CSS.HQ.Manager.Entities استفاده کنید. البته برای مورادی که از عناوین مخفف و یا برندهای خاص استفاده کرده‌اید از این قرارداد پیروی نکنید. مثلا نباید از عنوانی شبیه به Kara.CSS.Manager.IOs استفاده کنید.
5. از عنوانی پایدار و مستقل از نسخه محصول برای بخش دوم عنوان فضای نام استفاده کنید. بدین معنی که این عناوین با گذر زمان و تغییر و تحولات در محتوای محصول و یا تولیدکننده نباید تغییر کنند. مثال:
Microsoft.Reporting.WebForms
Kara.Support.Manager.Enums
Kara.CSS.HQ.WebUI.Configuration
6. از عناوین یکسان برای فضای نام و اشیای درون آن استفاده نکنید. مثلا نباید کلاسی با عنوان Manager در فضای نام Kara.CSS.Manager وجود داشته باشد.
7. از عناوین یکسان برای اشیای درون فضاهای نام یک برنامه استفاده نکنید. مثلا نباید دو کلاس با نام Package در فضاهای نام Kara.CSS.Manger و Kara.CSS.Manger.Entities داشته باشید.

ب. اصول نام‌گذاری کلاس‌ها و Structها
1. عنوان کلاس باید اسم یا موصوف باشد.
2. در نام‌گذاری کلاس‌ها باید از روش Pascal Casing استفاده شود.
3. نباید از عناوین مخففی که رایج نیستند استفاده کرد.
4. از پیشوندهای زائد مثل C یا Cls نباید استفاده شود.
5. نباید از کاراکترهایی به غیر از حروف (و یا در برخی موارد خیلی خاص، شماره نسخه) در نام‌گذاری کلاس‌ها استفاده شود.
مثال:
درست:
PackageManager , PacakgeConfigGenerator
Circle , Utility , Package
نادرست:
CreateConfig , classdata
CManager , ClsPackage , Config_Creator , Config1389
6. در نام‌گذاری اشیای Generic از استفاده از عناوینی چون Element, Node, Log و یا Message پرهیز کنید. این کار موجب به‌وجودآمدن کانفلیکت در عناوین اشیا می‌شود. در این موارد بهتر است از عناوینی چون FormElement, XmlNode, EventLog و SoapMessage استفاده شود. درواقع بهتر است نام اشیای جنریک، برای موارد موردنیاز، کاملا اختصاصی و درعین حال مشخص‌کننده محتوا و کاربرد باشند.
7. از عناوینی مشابه عناوین کتابخانه‌های پایه دات‌نت برای اشیای خود استفاده نکنید. مثلا نباید کلاسی با عنوان Console, Parameter, Action و یا Data را توسعه دهید. هم‌چنین از کابرد عناوینی مشابه اشیای موجود در فضاهای نام غیرپایه دات‌نت و یا غیردات‌نتی اما مورداستفاده در پروژه خود که محتوا و کاربردی مختص همان فضای نام دارند پرهیز کنید. مثلا نباید کلاسی با عنوان Page در صورت استفاده از فضای نام System.Web.UI تولید کنید.
8. سعی کنید در مواردی که مناسب به‌نظر می‌رسد از عنوان کلاس پایه در انتهای نام کلاس‌های مشتق‌شده استفاده کنید. مثل FileStream که از کلاس Stream مشتق شده است. البته این قاعده در تمام موارد نتیجه مطلوب ندارد. مثلا کلاس Button که از کلاس Control مشتق شده است. بنابراین در به‌کاربردن این مورد بهتر است تمام جوانب و قواعد را درنظر بگیرید.

پ. اصول نام‌گذاری مجموعه‌ها (Collections)
یک مجموعه در واقع یک نوع کلاس خاص است که حاوی مجموعه‌ای از داده‌هاست و از همان قوانین کلاس‌ها برای نام‌گذاری آن‌ها استفاده می‌شود.
1. بهتر است در انتهای عنوان مجموعه از کلمه Collection استفاده شود. مثال:
CenterCollection , PackageCollection
2. البته درصورتی‌که کلاس مورد نظر ما رابط IDictionary را پیاده‌سازی کرده باشد بهتر است از پسوند Dictionary استفاده شود.

ت. اصول نام‌گذاری Delegateها
1. عنوان یک delegate باید اسم یا موصوف باشد.
2. در نام‌گذاری delegateها باید از روش Pascal Casing استفاده شود.
3. نباید از عناوین مخففی که رایج نیستند استفاده کرد.
4. از پیشوندهای زائد مثل D یا del نباید استفاده شود.
5. نباید از کاراکترهایی به غیر از حروف در نام‌گذاری delegateها استفاده شود.
6. نباید در انتهای نام یک delegate از عبارت Delegate استفاده شود.
7. بهتر است که درصورت امکان در انتهای نام یک delegate که برای Event Handler استفاده نمی‌شود از عبارت Callback استفاده شود. البته تنها درصورتی‌که معنی و مفهوم مناسب را داشته باشد.
مثال:
نحوه تعریف یک delegate
public delegate void Logger (string log);
public delegate void LoggingCallback (object sender, string reason);

ث. اصول نام‌گذاری رویدادها (Events)
1. عنوان یک رویداد باید فعل یا مصدر باشد.
2. در نام‌گذاری کلاس‌ها باید از روش Pascal Casing استفاده شود.
3. نباید از عناوین مخففی که رایج نیستند استفاده کرد.
4. از پیشوندهای زائد نباید استفاده شود.
5. نباید از کاراکترهایی به غیر از حروف در نام‌گذاری رویدادها استفاده شود.
6. بهتر است از پسوند EventHandler در عنوان هندلر رویداد استفاده شود.
7. از به کاربردن عباراتی چون AfterXXX و یا BeforeXXX برای نمایش رویدادهای قبل و یا بعد از رخداد خاصی خودداری شود. به جای آن باید از اسم مصدر (شکل ingدار فعل) برای نام‌گذاری رویداد قبل از رخداد و هم‌چنین شکل گذشته فعل برای نام‌گذاری رویداد بعد از رخداد خاص استفاده کرد.
مثلا اگر کلاسی دارای رویداد Open باشد باید از عنوان Openning برای رویداد قبل از Open و از عنوان Opened برای رویداد بعد از Open استفاده کرد.
8. نباید از پیشوند On برای نام‌گذاری رویداد استفاده شود.
9. همیشه پارامتر e و sender را برای آرگومانهای رویداد پیاده سازی کنید. sender که از نوع object است نمایش دهنده شیی است که رویداد مربوطه را به وجود آورده است و e درواقع آرگومانهای رویداد مربوطه است.
10. عنوان کلاس‌آرگومان‌های رویداد باید دارای پسوند EventArgs باشد.
مثال:
عنوان کلاس‌آرگومان:
AddEventArgs , EditEventArgs , DeleteEventArgs
عنوان رویداد:
Adding , Add , Added
تعریف یک EventHandler:
public delegate void <EventName>EventHandler  (object sender, <EventName>EventArgs e);
نکته: نیاز به تولید یک EventHandler مختص توسعه یک برنامه به‌ندرت ایجاد می‌شود. در اکثر موارد می‌توان با استفاده از کلاس جنریک <EventHandler<TEventArgs تمام احتیاجات خود را برطرف کرد.
تعریف یک رویداد:
public event EventHandler <AddEventArgs> Adding;

ج. اصول نام‌گذاری Attributeها
1. عنوان یک attribute باید اسم یا موصوف باشد.
2. در نام‌گذاری attributeها باید از روش Pascal Casing استفاده شود.
3. نباید از عناوین مخففی که رایج نیستند استفاده کرد.
4. از پیشوندهای زائد مثل A یا atr نباید استفاده شود.
5. نباید از کاراکترهایی به غیر از حروف در نام‌گذاری attributeها استفاده شود.
6. بهتر است در انتهای نام یک attribute از عبارت Attribute استفاده شود.
مثال:
DisplayNameAttribute , MessageTypeAttribute

چ. اصول نام‌گذاری Interfaceها
1. در ابتدای عنوان interface باید از حرف I استفاده شود.
2. نام باید اسم، موصوف یا صفتی باشد که interface را توصیف می‌کند.
به عنوان مثال:
IComponent  (اسم)
IConnectionProvider (موصوف)
ICloneable (صفت)
3. از روش Pascal Casing استفاده شود.
4. خودداری از بکاربردن عبارات مخفف غیررایج.
5. باید تنها از کاراکترهای حرفی در نام interface استفاده شود.
 
ح. اصول نام‌گذاری Enumerationها
1. استفاده از روش Pascal Casing
2. خودداری از کاربرد عبارات مخفف غیررایج
3. تنها از کاراکترهای حرفی در نام Enumretionها استفاده شود.
4. نباید از پسوند یا پیشوند Enum یا Flag استفاده شود.
5. اعضای یک Enum نیز باید با روش Pascal Casing نام‌گذاری شوند.
مثال:
public enum FileMode {
    Append,
    Read, …
}
6. درصورتی‌که enum موردنظر از نوع flag نیست باید عنوان آن مفرد باشد. 
7. درصورتی‌که enum موردنظر برای کاربرد flag طراحی شده باشد نام آن باید جمع باشد. مثال:
[Flag]
public enum KeyModifiers {
    Alt = 1, 
    Control = 2,
    Shift = 4
}
8. از به‌کاربردن پسوند و یا پیشوندهای اضافه در نام‌گذاری اعضای یک enum نیز پرهیز کنید. مثلا نام‌گذاری زیر نادرست است:
public enum OperationState {
    DoneState, 
    FaultState,
    RollbackState
}

خ. اصول نام‌گذاری متدها
1. نام متد باید فعل یا ترکیبی از فعل و اسم یا موصوف باشد.
2. باید از روش Pascal Casing استفاده شود.
3. خودداری از بکاربردن عبارات مخفف غیررایج و یا استفاده زیاد از اختصار
4. تنها از کاراکترهای حرفی برای نام متد استفاده شود.
مثال:
AddDays , Save , DeleteRow , BindData , Close , Open

د. اصول نام‌گذاری Propertyها
1. نام باید اسم، صفت یا موصوف باشد.
2. باید از روش Pascal Casing استفاده شود.
3. خودداری از بکاربردن عبارات مخفف غیررایج.
4. تنها از کاراکترهای حرفی برای نام‌گذاری پراپرتی استفاده شود.
مثال:
Radius , ReportType , DataSource , Mode , CurrentCenterId
5. از عبارت Get در ابتدای هیچ Propertyای استفاده نکنید.
6. نام خاصیت‌هایی که یک مجموعه برمی‌گرداند باید به صورت جمع باشد. عنوان این Propertyها نباید به‌صورت مفرد به همراه پسوند Collection یا List باشد. مثال:
public CenterCollection Centers { get; set; }
7. خواص Boolean را با عناوینی مثبت پیاده‌سازی کنید. مثلا به‌جای استفاده از CantRead از CanRead استفاده کنید. بهتر است این Propertyها پیشوندهایی چون Is, Can یا Has داشته باشند، البته تنها درصورتی‌که استفاده از چنین پیشوندهایی ارزش افزوده داشته و مفهوم آن را بهتر برساند. مثلا عنوان CanSeek مفهوم روشن‌تری نسبت به Seekable دارد. اما استفاده از Created خیلی بهتر از IsCreated است یا Enabled کاربرد به مراتب راحت‌تری از IsEnabled دارد.
برای تشخیص بهتر این موارد بهتر است از روش ifسنجی استفاده شود. به عنوان مثال
if (list.Contains(item))
if (regularExpression.Matches(text))
if (stream.CanSeek)
if (context.Created)
if (form.Enabled)
مفهوم درست‌تری نسبت به موارد زیر دارند:
if (list.IsContains(item))
if (regularExpression.Match(text))
if (stream.Seekable)
if (context.IsCreated)
if (form.IsEnabled)
8. بهتر است در موارد مناسب عنوان Property با نام نوعش برابر باشد. مثلا
public Color Color { get; set; }

ذ. اصول نام‌گذاری پارامترها
پارامتر درحالت کلی به آرگومان وروی تعریف شده برای یک متد گفته می‌شود.
1. حتما از یک نام توصیفی استفاده شود. از نام‌گذاری پارامترها براساس نوعشان به‌شدت پرهیز کنید.
2. از روش camel Casing استفاده شود.
3. تنها از کاراکترهای حرفی برای نام‌گذاری پارامترها استفاده شود.
مثال:
firstName , e , id , packageId , centerName , name
4. نکاتی برای نام‌گذاری پارامترهای Operator Oveloading:
- برای operatorهای دو پارامتری (binary operators) از عناوین left و right برای پارامترهای آن استفاده کنید:
public static MyType operator +(MyType left, MyType right)
public static bool operator ==(MyType left, MyType right)
- برای operatorهای تک‌پارامتری (unary operators) اگر برای پارامتر مورد استفاده هیچ عنوان توصیفی مناسبی پیدا نکردید حتما از عبارت value استفاده کنید:
public static MyType operator ++(MyType value)
- درصورتی‌که استفاده از عناوین توصیفی دارای ارزش افزوده بوده و خوانایی کد را بهتر می‌کند حتما از این نوع عناوین استفاده کنید:
public static MyType operator /(MyType dividend, MyType divisor)
- نباید از عبارات مخفف یا عناوینی با اندیس‌های عددی استفاده کنید:
public static MyType operator -(MyType d1, MyType d2) // incorrect!

ر. اصول نام‌گذاری متغیر (Variable)ها
- نام متغیر باید اسم، صفت یا موصوف باشد.
نام‌گذاری متغیرها باید با توجه به نوع آن انجام شود.
1. متغیرهای عمومی (public) و protected و Constantها
- استفاده از روش Pascal Casing
- تنها از کاراکترهای حرفی برای نام متغیر عمومی استفاده شود.
مثال:
Area , DataBinder , PublicCacheName
2. متغیرهای private (در سطح کلاس یا همان field)
- نام این نوع متغیر باید با یک "_" شروع شود.
- از روش camel Casing استفاده شود.
مثال:
_centersList
_firstName
_currentCenter
3. متغیرهای محلی در سطح متد
- باید از روش camel Casing استفاده شود.
- تنها از کاراکترهای حرفی استفاده شود.
مثال:
parameterType , packageOperationTypeId

ز. اصول نام‌گذاری کنترل‌های UI
1. نام باید اسم یا موصوف باشد.
2. استفاده از روش Hungarian !
3. عبارت مخفف معرفی‌کننده کنترل، باید به اندازه کافی برای تشخیص نوع آن مناسب باشد.
مثال:
lblName (Label)
txtHeader (TextBox)
btnSave (Button)

ژ. اصول نام‌گذاری Exceptionها
تمام موارد مربوط به نام‌گذاری کلاس‌ها باید در این مورد رعایت شود. 
1. باید از پسوند Exception در انتهای عنوان استفاده شود.
مثال:
ArgumentNullException , InvalidOperaionException

س. نام‌گذاری اسمبلی‌ها و DLLها
1. عناوینی که برای نام‌گذاری اسمبلی‌ها استفاده می‌شوند، باید نمایش‌دهنده محتوای کلی آن باشند. مثل:
System.Data
2. از روش زیر برای نام‌گذاری اسمبلی‌ها استفاده شود:
<Company>.<Component>.dll
<Company>.<Project|Product|Technology>.<Component>.dll
مثل:
Microsoft.CSharp.dll , Kara.CSS.Manager.dll

ش. نام‌گذاری پارامترهای نوع (Generic (type parameter
1. از حرف T برای پارامترهای تک‌حرفی استفاده کنید. مثل:
public int IComparer<T> {…}
public delegate bool Predicate<T> (T item)
2. تمامی پارامترهای جنریک را با عناوینی توصیفی و مناسب که مفهوم و کاربرد آنرا برساند نام‌گذاری کنید، مگر آنکه یافتن چنین عباراتی ارزش افزوده‌ای در روشن‌تر کردن کد نداشته باشد. مثال:
public int ISessionChannel<TSession> {…}
public delegate TOutput Converter<TInput, TOutput> (TInput from)
public class Nullable<T> {…}
public class List<T> {…}
3. در ابتدای عناوین توصیفی حتما از حرف T استفاده کنید.
4. بهتر است تا به‌صورتی روشن نوع قید قرار داده شده بر روی پارامتری خاص را در نام آن پارامتر نمایش دهید. مثلا اگر قید ISession را برای پارامتری قرار دادید بهتر است نام آن پارامتر را TSession درنظر بگیرید.
 
ص. نام‌گذاری کلید Resourceها
به دلیل شباهت ساختاری که میان کلیدهای resource و propertyها وجود دارد قواعد نام‌گذاری propertyها در اینجا نیز معتبر هستند.
1. از روش نام‌گذاری Pascal Casing برای کلیدهای resource استفاده کنید.
2. از عناوین توصیفی برای این کلیدها استفاده کنید. سعی کنید تا حد امکان به هیچ وجه از عناوین کوتاه و یا مخففی که مفهوم را به‌صورت ناکامل می‌رساند استفاده نکنید. درواقع سعی کنید که خوانایی بیشتر کد را فدای فضای بیشتر نکنید.
3. از کلیدواژه‌های CLR و یا زبان مورداستفاده برای برنامه‌نویسی در نام‌گذاری این کلیدها استفاده نکنید.
4. تنها از حروف و اعداد و _ در نام‌گذاری این کلیدها استفاده کنید.
5. سعی کنید از عناوین توصیفی همانند زیر برای پیام‌های مناسب خطاها جهت نمایش به کاربر برای کلیدهای مربوطه استفاده کنید. درواقع نام کلید باید ترکیبی از نام نوع خطا و یک آی‌دی مشخص‌کننده پیغام مربوطه باشد:
ArgumentExceptionIllegalCharacters
ArgumentExceptionInvalidName
ArgumentExceptionFileNotFound
 
نکاتی درمورد کلمات مرکب
کلمات مرکب به کلماتی گفته می‌شود که در آن از بیش از یک کلمه با مفهوم مستقل استفاده شده باشد. مثل Callback یا FileName. 
باید توجه داشت که با تمام کلمات موجود در یک کلمه مرکب نباید همانند یک کلمه مستقل رفتار کرد و حرف اول آن را در روش‌های نام‌گذاری موجود به‌صورت بزرگ نوشت. کلمات مرکبی وجود دارند که به آن‌ها closed-form گفته می‌شود. این کلمات مرکب با اینکه از 2 یا چند کلمه دارای مفهوم مستقل تشکیل شده‌اند اما به‌خودی‌خود دارای مفهوم جداگانه و مستقلی هستند. برای تشخیص این کلمات می‌توان به فرهنگ لغت مراجعه کرد (و یا به‌سادگی از نرم‌افزار Microsoft Word استفاده کرد) و دریافت که آیا کلمه مرکب موردنظر آیا مفهوم مستقلی برای خود دارد، یعنی درواقع آیا عبارتی closed-form است. با کلمات مرکب از نوع closed-form همانند یک کلمه ساده برخورد می‌شود! 
در جدول زیر مثال‌هایی از عبارات رایج و نحوه درست و نادرست استفاده از هریک نشان داده شده است.

Pascal Casing

camel Casing

Wrong

Callback

callback

CallBack

BitFlag

bitFlag

Bitflag / bitflag

Canceled

canceled

Cancelled

DoNot

doNot

Donot / Don’t

Email

email

EMail

Endpoint

endpoint

EndPoint / endPoint

FileName

fileName

Filename / filename

Gridline

gridline

GridLine / gridLine

Hashtable

hashtable

HashTable / hashTable

Id

id

ID

Indexes

indexes

Indices

LogOff

logOff

Logoff / LogOut !

LogOn

logOn

Logon / LogIn !

SignOut

signOut

Signout / SignOff

SignIn

signIn

Signin / SignOn

Metadata

metadata

MetaData / metaData

Multipanel

multipanel

MultiPanel / multiPanel

Multiview

multiview

MultiView / multiView

Namespace

namespace

NameSpace / nameSpace

Ok

ok

OK

Pi

pi

PI

Placeholder

placeholder

PlaceHolder / placeHolder

UserName

username

Username / username

WhiteSpace

whiteSpace

Whitespace / whitespace

Writable

writable

Writeable / writeable

همان‌طور که در جدول بالا مشاهد می‌شود در استفاده از قوانین عبارات مخفف دو مورد استثنا وجود دارد که عبارتند از Id و Ok. این کلمات باید همان‌طور که در اینجا نشان داده شده‌اند استفاده شوند.
 
نکاتی درباره عبارات مخفف
1. عبارات مخفف (Acronym) با خلاصه‌سازی کلمات (Abbreviation) فرق دارند. یک عبارت مخفف شامل حروف اول یک عبارت طولانی یا معروف است، درصورتی‌که خلاصه‌سازی یک عبارت یا کلمه از حذف بخشی از آن به‌دست می‌آید. تا آنجاکه امکان دارد از خلاصه‌سازی عبارات نباید استفاده شود. هم‌چنین استفاده از عبارات مخفف غیررایج توصیه نمی‌شود.
مثال: IO و UI
2. براساس تعریف، یک مخفف حداقل باید 2 حرف داشته باشد. نحوه برخورد با مخفف‌های دارای بیشتر از 2 حرف با مخفف‌های دارای 2 حرف باهم متفاوت است. با مخفف‌های دارای 2 حرف همانند کلمه‌ای یک حرفی! برخورد می‌شود، درصورتی‌که با سایر مخفف‌ها همانند یک کلمه کامل چندحرفی برخورد می‌شود. به‌عنوان مثال IOStream برای روش PascalCasing و ioStream برای روش camelCasing استفاده می‌شود. هم‌چنین از HtmlBody و htmlBody به‌ترتیب برای روش‌های Pascal و camel استفاده می‌شود.
3. هیچ‌کدام از حروف یک عبارت مخفف در ابتدای یک واژه نام‌گذاری‌شده به روش camelCasing به‌صورت بزرگ نوشته نمی‌شود!
نکته: موارد زیادی را می‌توان یافت که در ابتدا به‌نظر می‌رسد برای پیاده‌سازی آنها باید قوانین فوق را نقض کرد. این موارد شامل استفاده از کتابخانه‌های سایر پلتفرم‌ها (مثل MFC, HTML و غیره)، جلوگیری از مشکلات جغرافیایی! (مثلا در مورد نام کشورها یا سایر موقعیت‌های جغرافیایی)، احترام به نام افراد درگذشته، و مواردی از این دست می‌شود. اما در بیشتر قریب به اتفاق این موارد هم می‌توان بدون تقض قوانین فوق اقدام به نام‌گذاری اشیا کرد، بدون اینکه با تغییر عبارت اصلی (که موجب تطابق با این قوانین می‌شود) لطمه‌ای به مفهوم آن بزند. البته تنها موردی که به‌نظر می‌رسد می‌تواند قوانین فوق را نقض کند نام‌های تجاری هستند. البته استفاده از نام‌های تجاری توصیه نمی‌شود، چون این نام‌ها سریع‌تر از محتوای کتابخانه‌های برنامه‌نویسان تغییر می‌کنند!
نکته: برای درک بهتر قانون "عدم استفاده از عبارات مخففی که رایج نیستند" مثالی از زبان توسعه دهندگان دات‌نت‌فریمورک ذکر می‌شود. در کلاس Color متد زیر با Overloadهای مختلف در دسترس است:
public class Color {
    …
    public static Color FromArgb(…)
    { … }
}
همان‌طور که مشاهده می‌شود برای رعایت قوانین فوق به‌جای استفاده از ARGB از عبارت Argb استفاده شده است. اما این نحوه استفاده موجب شده تا این سوال به‌ظاهر خنده‌دار اما درست پیش‌آید:
"چطور می‌شود در این کلاس رنگی را از ARGB تبدل کرد؟ هرچه که من میبینم فقط تبدیل از طریق (Argb) آرگومان b است!"
حال در این نقطه به‌نظر می‌رسد که در اینجا باید قوانین فوق را نقض کرد و از عنوان FromARGB که مفهوم درست را می‌رساند استفاده کرد. اما با کمی دقت متوجه می‌شویم که این قوانین در ابتدا نیز با پیاده‌سازی نشان داده شده در قطعه کد بالا نقض شده‌اند! همه می‌دانیم که عبارت RGB مخفف معروفی برای عبارت Red Green Blue است. اما استفاده از ARGB برای افزودن کلمه Alpha به ابتدای عبارت مذکور چندان رایج نیست. پس استفاده از مخفف Argb از همان ابتدا اشتباه به‌نظر می‌رسد. بنابراین راه‌حل بهتر می‌تواند استفاده از عنوان FromAlphaRgb باشد که هم قوانین فوق را نقض نکرده و هم مفهوم را بهتر می‌رساند.
 
دیگر نکات
1. قراردادهای اشاره شده در این سند حاصل کار شبانه روزی تعداد بسیاری از برنامه نویسان در سرتاسر جهان در پروژه‌های بزرگ بوده است. این اصول کلی تنها برای توسعه آسان‌تر و سریع‌تر پروژه‌های بزرگ تعیین شده‌اند و همان‌طور که روشن است تنها ازطریق تجربه دست‌یافتنی هستند. بنابراین چه بهتر است که در این راه از تجارب بزرگان این عرصه بیشترین بهره برده شود.
2. هم‌چنین توجه داشته باشید که بیشتر قراردادهای اشاره شده در این سند از راهنمای نام‌گذاری تیم توسعه BCL دات‌نت‌فریمورک گرفته شده است. این تیم طبق اعتراف خودشان زمان بسیار زیادی را برای نام‌گذاری اشیا صرف کرده‌اند و توصیه کرده‌اند که دیگران نیز برای نام‌گذاری، زمان مناسب و کافی را در توسعه پروژه‌ها درنظر بگیرند.
 
نظرات مطالب
روش‌هایی برای بهبود تجربه‌ی کاربری صفحات لاگین و ثبت نام
وجود ندارد به معنای عدم امکان افزودن آن‌ها نیست. تمام کامپوننت‌های استاندارد Blazor به همراه خاصیت زیر هم هستند:
[Parameter(CaptureUnmatchedValues = true)] 
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
با تعریف پارامتر از نوع CaptureUnmatchedValues = true، تمام ویژگی‌های اضافه شده که به صورت پارامتر عمومی کامپوننت نیستند، به عنوان Unmatched Values تفسیر شده و مورد استفاده قرار می‌گیرند. یعنی همینقدر که ویژگی "autocomplete="new-password را به تعریف کامپوننت اضافه کردید (و هر مورد مشابه دیگری را)، یک Unmatched Value است و به صورت خودکار در حین رندر نهایی، در المان اضافه شده‌ی به صفحه، ظاهر می‌شود.
مطالب
React 16x - قسمت 28 - احراز هویت و اعتبارسنجی کاربران - بخش 3 - فراخوانی منابع محافظت شده و مخفی کردن عناصر صفحه
ارسال خودکار هدرهای ویژه‌ی Authorization، به سمت سرور

در برنامه‌ی backend این سری (که از انتهای مطلب قابل دریافت است)، به Controllers\MoviesController.cs مراجعه کرده و متدهای Get/Delete/Create آن‌را با فیلتر [Authorize] مزین می‌کنیم تا دسترسی به آن‌ها، تنها به کاربران لاگین شده‌ی در سیستم، محدود شود. در این حالت اگر به برنامه‌ی React مراجعه کرده و برای مثال سعی در ویرایش رکوردی کنیم، اتفاقی رخ نخواهد داد:


علت را نیز در برگه‌ی network کنسول توسعه دهندگان مرورگر، می‌توان مشاهده کرد. این درخواست از سمت سرور با Status Code: 401، برگشت خورده‌است. برای رفع این مشکل باید JSON web token ای را که در حین لاگین، از سمت سرور دریافت کرده بودیم، به همراه درخواست خود، مجددا به سمت سرور ارسال کنیم. این ارسال نیز باید به صورت یک هدر مخصوص با کلید Authorization و مقدار "Bearer jwt" باشد.
به همین جهت ابتدا به src\services\authService.js مراجعه کرده و متدی را برای بازگشت JWT ذخیره شده‌ی در local storage به آن اضافه می‌کنیم:
export function getLocalJwt(){
  return localStorage.getItem(tokenKey);
}
سپس به src\services\httpService.js مراجعه کرده و از آن استفاده می‌کنیم:
import * as auth from "./authService";

axios.defaults.headers.common["Authorization"] = "Bearer " + auth.getLocalJwt();
کار این یک سطر که در ابتدای ماژول httpService قرار می‌گیرد، تنظیم هدرهای پیش‌فرض تمام انواع درخواست‌های ارسالی توسط Axios است. البته می‌توان از حالت‌های اختصاصی‌تری نیز مانند فقط post، بجای common استفاده کرد. برای نمونه در تنظیم فوق، تمام درخواست‌های HTTP Get/Post/Delete/Put ارسالی توسط Axios، دارای هدر Authorization که مقدار آن به ثابتی شروع شده‌ی با Bearer و سپس مقدار JWT دریافتی از سرور تنظیم می‌شود، خواهند بود.

مشکل! اگر برنامه را در این حالت اجرا کنید، یک چنین خطایی را مشاهده خواهید کرد:
Uncaught ReferenceError: Cannot access 'tokenKey' before initialization
علت اینجا است که سرویس httpService، دارای ارجاعی به سرویس authService شده‌است و برعکس (در httpService، یک import از authService را داریم و در authService، یک import از httpService را)! یعنی یک وابستگی حلقوی و دو طرفه رخ‌داده‌است.
برای رفع این خطا باید ابتدا مشخص کنیم که کدامیک از این ماژول‌ها، اصلی است و کدامیک باید وابسته‌ی به دیگری باشد. در این حالت httpService، ماژول اصلی است و بدون آن و با نبود امکان اتصال به backend، دیگر authService قابل استفاده نخواهد بود.
به همین جهت به httpService مراجعه کرده و import مربوط به authService را از آن حذف می‌کنیم. سپس در همینجا متدی را برای تنظیم هدر Authorizationاضافه کرده و آن‌را به لیست default exports این ماژول نیز اضافه می‌کنیم:
function setJwt(jwt) {
  axios.defaults.headers.common["Authorization"] = "Bearer " + jwt;
}

//...

export default {
  // ...
  setJwt
};
در آخر، در authService که ارجاعی را به httpService دارد، فراخوانی متد setJwt را در ابتدای ماژول، انجام خواهیم داد:
http.setJwt(getLocalJwt());
به این ترتیب، وابستگی حلقوی بین این دو ماژول برطرف می‌شود و اکنون این authService است که به httpService وابسته‌است و نه برعکس.

تا اینجا اگر تغییرات را ذخیره کرده و سعی در ویرایش یکی از رکوردهای فیلم‌های نمایش داده شده کنیم، این‌کار با موفقیت انجام می‌شود؛ چون اینبار درخواست ارسالی، دارای هدر ویژه‌ی authorization است:



روش بررسی انقضای توکن‌ها در سمت کلاینت

اگر JWT قدیمی و منقضی شده‌ی از روز گذشته را آزمایش کنید، باز هم از سمت سرور، Status Code: 401 دریافت خواهد شد. اما اینبار در لاگ‌های برنامه‌ی سمت سرور، OnChallenge error مشخص است. در این حالت باید یکبار logout کرد تا JWT قدیمی حذف شود. سپس نیاز به لاگین مجدد است تا یک JWT جدید دریافت گردد. می‌توان اینکار را پیش از ارسال اطلاعات به سمت سرور، در سمت کلاینت نیز بررسی کرد:
function checkExpirationDate(user) {
  if (!user || !user.exp) {
    throw new Error("This access token doesn't have an expiration date!");
  }

  user.expirationDateUtc = new Date(0); // The 0 sets the date to the epoch
  user.expirationDateUtc.setUTCSeconds(user.exp);

  const isAccessTokenTokenExpired =
    user.expirationDateUtc.valueOf() < new Date().valueOf();
  if (isAccessTokenTokenExpired) {
    throw new Error("This access token is expired!");
  }
}
در اینجا user، همان شیء حاصل از const user = jwtDecode(jwt) است که در قسمت قبل به آن پرداختیم. سپس خاصیت exp آن با زمان جاری مقایسه شده و در صورت وجود مشکلی، استثنایی را صادر می‌کند. می‌توان این متد را پس از فراخوانی jwtDecode، قرار داد.


محدود کردن حذف رکوردهای فیلم‌ها به نقش Admin در Backend

تا اینجا تمام کاربران وارد شده‌ی به سیستم، می‌توانند علاوه بر ویرایش فیلم‌ها، آن‌ها را نیز حذف کنند. به همین جهت می‌خواهیم دسترسی حذف را از کاربرانی که ادمین نیستند، بگیریم. برای این منظور، در سمت سرور کافی است در کنترلر MoviesController، ویژگی [Authorize(Policy = CustomRoles.Admin)] را به اکشن متد Delete، اضافه کنیم. به این ترتیب اگر کاربری در سیستم ادمین نبود و درخواست حذف رکوردی را صادر کرد، خطای 403 را از سمت سرور دریافت می‌کند:


در برنامه‌ی مثال backend این سری، در فایل Services\UsersDataSource.cs، یک کاربر ادمین پیش‌فرض ثبت شده‌است. مابقی کاربرانی که به صورت معمولی در سایت ثبت نام می‌کنند، ادمین نیستند.
در این حالت اگر کاربری ادمین بود، چون در توکن او که در فایل Services\TokenFactoryService.cs صادر می‌شود، یک User Claim ویژه‌ی از نوع Role و با مقدار Admin وجود دارد:
if (user.IsAdmin)
{
   claims.Add(new Claim(ClaimTypes.Role, CustomRoles.Admin, ClaimValueTypes.String, _configuration.Value.Issuer));
}
این مقدار، در payload توکن نهایی او نیز ظاهر خواهد شد:
{
  // ...
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  // ...
}
بنابراین هربار که برنامه‌ی React ما، هدر Bearer jwt را به سمت سرور ارسال می‌کند، فیلتر Authorize محدود شده‌ی به نقش ادمین، این نقش را در صورت وجود در توکن او، پردازش کرده و دسترسی‌های لازم را به صورت خودکار صادر می‌کند و همانطور که پیشتر نیز عنوان شد، اگر کاربری این نقش را به صورت دستی به توکن ارسالی به سمت سرور اضافه کند، به دلیل دسترسی نداشتن به کلیدهای خصوصی تولید مجدد امضای دیجیتال توکن، درخواست او در سمت سرور تعیین اعتبار نشده و برگشت خواهد خورد.


نکته 1: اگر در اینجا چندین بار یک User Claim را با مقادیر متفاوتی، به لیست claims اضافه کنیم، مقادیر آن در خروجی نهایی، به شکل یک آرایه ظاهر می‌گردند.

نکته 2: پیاده سازی سمت سرور backend این سری، یک باگ امنیتی مهم را دارد! در حین ثبت نام، کاربران می‌توانند مقدار خاصیت isAdmin شیء User را:
    public class User : BaseModel
    {
        [Required, MinLength(2), MaxLength(50)]
        public string Name { set; get; }

        [Required, MinLength(5), MaxLength(255)]
        public string Email { set; get; }

        [Required, MinLength(5), MaxLength(1024)]
        public string Password { set; get; }

        public bool IsAdmin { set; get; }
    }
خودشان دستی تنظیم کرده و ارسال کنند تا به صورت ادمین ثبت شوند! به این مشکل مهم، اصطلاحا mass assignment گفته می‌شود.
راه حل اصولی مقابله‌ی با آن، داشتن یک DTO و یا ViewModel خاص قسمت ثبت نام و جدا کردن مدل متناظر با موجودیت User، از شیءای است که اطلاعات نهایی را از کاربر، دریافت می‌کند. شیءای که اطلاعات را از کاربر دریافت می‌کند، نباید دارای خاصیت isAdmin قابل تنظیم در حین ثبت نام معمولی کاربران سایت باشد. یک روش دیگر حل این مشکل، استفاده از ویژگی Bind و ذکر صریح نام خواصی است که قرار است bind شوند و نه هیچ خاصیت دیگری از شیء User:
[HttpPost]
public ActionResult<User> Create(
            [FromBody]
            [Bind(nameof(Models.User.Name), nameof(Models.User.Email), nameof(Models.User.Password))]
            User data)
        {
و یا حتی می‌توان ویژگی [BindNever] را بر روی خاصیت IsAdmin، در مدل User قرار داد.

نکته 3: اگر می‌خواهید در برنامه‌ی React، با مواجه شدن با خطای 403 از سمت سرور، کاربر را به یک صفحه‌ی عمومی «دسترسی ندارید» هدایت کنید، می‌توانید از interceptor سراسری که در قسمت 24 تعریف کردیم، استفاده کنید. در اینجا status code = 403 را جهت history.push به یک آدرس access-denied سفارشی و جدید، پردازش کنید.


نمایش یا مخفی کردن المان‌ها بر اساس سطوح دسترسی کاربر وارد شده‌ی به سیستم

می‌خواهیم در صفحه‌ی نمایش لیست فیلم‌ها، دکمه‌ی new movie را که بالای صفحه قرار دارد، به کاربرانی که لاگین نکرده‌اند، نمایش ندهیم. همچنین نمی‌خواهیم اینگونه کاربران، بتوانند فیلمی را ویرایش و یا حذف کنند؛ یعنی لینک به صفحه‌ی جزئیات ویرایشی فیلم‌ها و ستونی که دکمه‌ها‌ی حذف هر ردیف را نمایش می‌دهد، به کاربران وارد نشده‌ی به سیستم نمایش داده نشوند.

در قسمت قبل، در فایل app.js، شیء currentUser را به state اضافه کردیم و با استفاده از ارسال آن به کامپوننت NavBar:
<NavBar user={this.state.currentUser} />
 نام کاربر وارد شده‌ی به سیستم را نمایش دادیم. با استفاده از همین روش می‌توان شیء currentUser را به کامپوننت Movies ارسال کرد و سپس بر اساس محتوای آن، قسمت‌های مختلف صفحه را مخفی کرد و یا نمایش داد. البته در اینجا (در فایل app.js) خود کامپوننت Movies درج نشده‌است؛ بلکه مسیریابی آن‌را تعریف کرده‌ایم که با روش ارسال پارامتر به یک مسیریابی، در قسمت 15، قابل تغییر و پیاده سازی است:
<Route
   path="/movies"
   render={props => <Movies {...props} user={this.state.currentUser} />}
/>
در اینجا برای ارسال props به یک کامپوننت، نیاز است از ویژگی render استفاده شود. سپس پارامتر arrow function را به همان props تنظیم می‌کنیم. همچنین با استفاده از spread operator، این props را در المان JSX تعریف شده، گسترده و تزریق می‌کنیم؛ تا از سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، محروم نشویم و آن‌ها را از دست ندهیم. در نهایت المان کامپوننت مدنظر را همانند روش متداولی که برای تعریف تمام کامپوننت‌های React و تنظیم ویژگی‌های آن‌ها استفاده می‌شود، بازگشت می‌دهیم.

پس از این تغییر به فایل src\components\movies.jsx مراجعه کرده و شیء user را در متد رندر، دریافت می‌کنیم:
class Movies extends Component {
  // ...

  render() {
    const { user } = this.props;
    // ...
اکنون که در کامپوننت Movies به این شیء user دسترسی پیدا کردیم، توسط آن می‌توان قسمت‌های مختلف صفحه را مخفی کرد و یا نمایش داد:
{user && (
  <Link
    to="/movies/new"
    className="btn btn-primary"
    style={{ marginBottom: 20 }}
  >
    New Movie
  </Link>
)}
با این تغییر، اگر شیء user مقدار دهی شده باشد، عبارت پس از &&، در صفحه درج خواهد شد و برعکس:


در این تصویر همانطور که مشخص است، کاربر هنوز به سیستم وارد نشده‌است؛ بنابراین به علت null بودن شیء user، دکمه‌ی New Movie را مشاهده نمی‌کند.


روش دریافت نقش‌های کاربر وارد شده‌ی به سیستم در سمت کلاینت

همانطور که پیشتر در مطلب جاری عنوان شد، نقش‌های دریافتی از سرور، یک چنین شکلی را در jwtDecode نهایی (یا user در اینجا) دارند:
{
  // ...
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  // ...
}
که البته اگر چندین Role تعریف شده باشند، مقادیر آن‌ها در خروجی نهایی، به شکل یک آرایه ظاهر می‌گردد. بنابراین برای بررسی آن‌ها می‌توان نوشت:
function addRoles(user) {
  const roles =
    user["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"];
  if (roles) {
    if (Array.isArray(roles)) {
      user.roles = roles.map(role => role.toLowerCase());
    } else {
      user.roles = [roles.toLowerCase()];
    }
  }
}
کار این متد، دریافت نقش و یا نقش‌های ممکن از jwtDecode، و بازگشت آن‌ها (افزودن آن‌ها به صورت یک خاصیت جدید، به نام roles، به شیء user دریافتی) به صورت یک آرایه‌ی با عناصری LowerCase است. سپس اگر نیاز به بررسی نقش‌، یا نقش‌های کاربری خاص بود، می‌توان از یکی از متدهای زیر استفاده کرد:
export function isAuthUserInRoles(user, requiredRoles) {
  if (!user || !user.roles) {
    return false;
  }

  if (user.roles.indexOf(adminRoleName.toLowerCase()) >= 0) {
    return true; // The `Admin` role has full access to every pages.
  }

  return requiredRoles.some(requiredRole => {
    if (user.roles) {
      return user.roles.indexOf(requiredRole.toLowerCase()) >= 0;
    } else {
      return false;
    }
  });
}

export function isAuthUserInRole(user, requiredRole) {
  return isAuthUserInRoles(user, [requiredRole]);
}
متد isAuthUserInRoles، آرایه‌ای از نقش‌ها را دریافت می‌کند و سپس بررسی می‌کند که آیا کاربر انتخابی، دارای این نقش‌ها هست یا خیر و متد isAuthUserInRole، تنها یک نقش را بررسی می‌کند.
در این کدها، adminRoleName به صورت زیر تامین شده‌است:
import { adminRoleName, apiUrl } from "../config.json";
یعنی محتویات فایل config.json تعریف شده را به صورت زیر با افزودن نام نقش ادمین، تغییر داده‌ایم:
{
  "apiUrl": "https://localhost:5001/api",
  "adminRoleName": "Admin"
}


عدم نمایش ستون Delete ردیف‌های لیست فیلم‌ها، به کاربرانی که Admin نیستند

اکنون که امکان بررسی نقش‌های کاربر لاگین شده‌ی به سیستم را داریم، می‌خواهیم ستون Delete ردیف‌های لیست فیلم‌ها را فقط به کاربری که دارای نقش Admin است، نمایش دهیم. برای اینکار نیاز به دریافت شیء user، در src\components\moviesTable.jsx وجود دارد. یک روش دریافت کاربر جاری وارد شده‌ی به سیستم، همانی است که تا به اینجا بررسی کردیم: شیء currentUser را به صورت props، از بالاترین کامپوننت، به پایین‌تر کامپوننت موجود در component tree ارسال می‌کنیم. روش دیگر اینکار، دریافت مستقیم کاربر جاری از خود src\services\authService.js است و ... اینکار ساده‌تر است! به علاوه اینکه همیشه بررسی تاریخ انقضای توکن را نیز به صورت خودکار انجام می‌دهد و در صورت انقضای توکن، کاربر را در قسمت catch متد getCurrentUser، از سیستم خارج خواهد کرد.
بنابراین در src\components\moviesTable.jsx، ابتدا authService را import می‌کنیم:
import * as auth from "../services/authService";
در ادامه ابتدا تعریف ستون حذف را از آرایه‌ی columns خارج کرده و تبدیل به یک خاصیت می‌کنیم. یعنی در ابتدای کار، چنین ستونی تعریف نشده‌است:
class MoviesTable extends Component {
  columns = [ ... ];
  // ...

  deleteColumn = {
    key: "delete",
    content: movie => (
      <button
        onClick={() => this.props.onDelete(movie)}
        className="btn btn-danger btn-sm"
      >
        Delete
      </button>
    )
  };
در آخر در متد سازنده‌ی این کامپوننت، کاربر جاری را از authService دریافت کرده و اگر این کاربر دارای نقش Admin بود، ستون deleteColumn را به لیست ستون‌های موجود، اضافه می‌کنیم تا نمایش داده شود:
  constructor() {
    super();
    const user = auth.getCurrentUser();
    if (user && auth.isAuthUserInRole(user, "Admin")) {
      this.columns.push(this.deleteColumn);
    }
  }
اکنون برای آزمایش برنامه، یکبار از آن خارج شوید؛ دیگر نباید ستون Delete نمایش داده شود. همچنین یکبار هم تحت عنوان یک کاربر معمولی در سایت ثبت نام کنید. این کاربر نیز چنین ستونی را مشاهده نمی‌کند.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-28-backend.zip و sample-28-frontend.zip
مطالب
آموزش WAF (بررسی ساختار همراه با پیاده سازی یک مثال)
در این پست با مفاهیم اولیه این کتابخانه آشنا شدید. برای بررسی و پیاده سازی مثال، ابتدا یک Blank Solution را ایجاد نمایید. فرض کنید قصد پیاده سازی یک پروژه بزرگ ماژولار را داریم. برای این کار لازم است مراحل زیر را برای طراحی ساختار مناسب پروژه دنبال نمایید.
نکته: آشنایی اولیه با مفاهیم MEF از ملزومات این بخش است.
»ابتدا یک Class Library به نام Views ایجاد نمایید و اینترفیس زیر را به صورت زیر در آن تعریف نمایید. این اینترفیس رابط بین کنترلر و View از طریق ViewModel خواهد بود.
 public interface IBookView : IView
    {
        void Show();
        void Close();
    }
اینترفیس IView در مسیر System.Waf.Applications قرار دارد. در نتیجه از طریق nuget اقدام به نصب Package  زیر نمایید:
Install-Package WAF
»حال در Solution ساخته شده  یک پروژه از نوع WPF Application به نام Shell ایجاد کنید. با استفاده از نیوگت، Waf Package را نصب نمایید؛ سپس ارجاعی از اسمبلی Views را به آن ایجاد کنید. output type اسمبلی Shell را به نوع ClassLibrary تغییر داده، همچنین فایل‌های موجود در آن را حذف نمایید. یک فایل Xaml جدید را به نام BookShell ایجاد نمایید و کد‌های زیر را در آن کپی نمایید:
<Window x:Class="Shell.BookShell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Book View" Height="350" Width="525">
    <Grid>
        <DataGrid ItemsSource="{Binding Books}" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="400" Height="200">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Code" Binding="{Binding Code}" Width="100"></DataGridTextColumn>
                <DataGridTextColumn Header="Title" Binding="{Binding Title}" Width="300"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>

    </Grid>
</Window>
این فرم فقط شامل یک دیتاگرید برای نمایش اطلاعات کتاب‌هاست. دیتای آن از طریق ViewModel تامین خواهد شد، در نتیجه ItemsSource آن به خاصیتی به نام Books بایند شده است. حال ارجاعی به اسمبلی System.ComponentModel.Composition دهید. سپس در Code behind این فرم کد‌های زیر را کپی کنید:
[Export(typeof(IBookView))]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public partial class BookShell : Window, IBookView
    {
        public BookShell()
        {
            InitializeComponent();
        }
    }
کاملا واضح است که این فرم اینترفیس IBookView را پیاده سازی کرده است. از آنجاکه کلاس Window به صورت پیش فرض دارای متد‌های Show و Close است در نتیجه نیازی به پیاده سازی مجدد متدهای IBookView نیست. دستور Export باعث می‌شود که این کلاس به عنوان وابستگی به Composition Container اضافه شود تا در جای مناسب بتوان از آن وهله سازی کرد. نکته‌ی مهم این است که به دلیل آنکه این کلاس، اینترفیس IBookView را پیاده سازی کرده است در نتیجه نوع Export این کلاس حتما باید به صورت صریح از نوع IBookView باشد.

»یک Class Library به نام Models بسازید و بعد از ایجاد آن، کلاس زیر را به عنوان مدل Book در آن کپی کنید:
 public class Book
    {
        public int Code { get; set; }

        public string Title { get; set; }
    }
»یک  Class Library دیگر به نام ViewModels ایجاد کنید و همانند مراحل قبلی، Package مربوط به WAF را نصب کنید. سپس کلاسی به نام BookViewModel ایجاد نمایید و کدهای زیر را در آن کپی کنید (ارجاع به اسمبلی‌های Views و Models را فراموش نکنید):
[Export]
    [Export(typeof(ViewModel<IBookView>))]
    public class BookViewModel : ViewModel<IBookView>
    {
        [ImportingConstructor]
        public BookViewModel(IBookView view)
            : base(view)
        {
        }
       
        public ObservableCollection<Book> Books { get; set; }
        
    }
ViewModel مورد نظر از کلاس ViewModel of T ارث برده است. نوع این کلاس معادل نوع View مورد نظر ماست که در اینجا مقصود IBookView است. این کلاس شامل خاصیتی به نام ViewCore است که امکان فراخوانی متد‌ها و خاصیت‌های View را فراهم می‌نماید. وظیفه اصلی کلاس پایه ViewModel، وهله سازی از View سپس ست کردن خاصیت DataContext در View مورد نظر به نمونه وهله سازی شده از ViewModel است. در نتیجه عملیات مقید سازی در Shell به درستی انجام خواهدشد.
به دلیل اینکه سازنده پیش فرض در  این کلاس وجود ندارد حتما باید از ImportingConstructor استفاده نماییم تا CompositionContainer در هنگام عملیات وهله سازی Exception صادر نکند.

»بخش بعدی ساخت یک Class Library دیگر به نام Controllers است. در این Library نیز بعد از ارجاع به اسمبلی‌های زیر کتابخانه WAF را نصب نمایید. 
  • Views
  • Models
  • ViewModels
  • System.ComponentModel.Composition
کلاسی به نام BookController بسازید و کد‌های زیر را در آن کپی نمایید:
[Export]
    public class BookController
    {
        [ImportingConstructor]
        public BookController(BookViewModel viewModel)
        {
            ViewModelCore = viewModel;
        }

        public BookViewModel ViewModelCore
        {
            get;
            private set;
        }

        public void Run()
        {
            var result = new List<Book>();
            result.Add(new Book { Code = 1, Title = "Book1" });
            result.Add(new Book { Code = 2, Title = "Book2" });
            result.Add(new Book { Code = 3, Title = "Book3" });

            ViewModelCore.Books = new ObservableCollection<Models.Book>(result);

            (ViewModelCore.View as IBookView).Show();
        }
    }
نکته مهم این کلاس این است که BookViewModel به عنوان وابستگی این کنترلر تعریف شده است. در نتیجه در هنگام وهله سازی از این کنترلر Container مورد نظر یک وهله از BookViewModel را در اختیار آن قرار خواهد داد. در متد Run نیز ابتدا مقدار Book که به ItemsSource دیتا گرید در BookShell مقید شده است مقدار خواهد گرفت. سپس با فراخوانی متد Show از اینترفیس IBookView، متد Show در BookShell فراخوانی خواهد شد که نتیجه آن نمایش فرم مورد نظر است.

طراحی Bootstrapper

در پروژه‌های ماژولار  Bootstrapper از ملزومات جدانشدنی این گونه پروژه هاست. برای این کار ابتدا یک WPF Application دیگر به نام Bootstrapper ایجاد نماید. سپس ارجاعی به اسمبلی‌های زیر را در آن قرار دهید:
»Controllers
»Views
»ViewModels
»Shell
»System.ComponentModel.Composition
»نصب بسته WAF با استفاده از nuget

حال یک کلاس به نام  AppBootstrapper ایجاد نمایید و کد‌های زیر را در آن کپی نمایید:
public class AppBootstrapper
    {
        public CompositionContainer Container
        {
            get;
            private set;
        }

        public AggregateCatalog Catalog
        {
            get;
            private set;
        }

        public void Run()
        {
            Catalog = new AggregateCatalog();
            Catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
            
            Catalog.Catalogs.Add(new AssemblyCatalog(String.Format("{0}\\{1}", Environment.CurrentDirectory, "Shell.dll")));
            Catalog.Catalogs.Add(new AssemblyCatalog(String.Format("{0}\\{1}", Environment.CurrentDirectory, "ViewModels.dll")));
            Catalog.Catalogs.Add(new AssemblyCatalog(String.Format("{0}\\{1}", Environment.CurrentDirectory, "Controllers.dll")));

            Container = new CompositionContainer(Catalog);

            var batch = new CompositionBatch();
            batch.AddExportedValue(Container);
            Container.Compose(batch);

            var bookController = Container.GetExportedValue<BookController>();
            bookController.Run();


        }
    }
اگر با MEF آشنا باشید کد‌های بالا نیز برای شما مفهوم مشخصی دارند. در متد Run این کلاس ابتدا Catalog ساخته می‌شود. سپس با اسکن اسمبلی‌های مورد نظر تمام Export‌ها و Import‌های لازم واکشی شده و به Conrtainer مورد نظر رجیستر می‌شوند. در انتها نیز با وهله سازی از BookController و فراخوانی متد Run آن خروجی زیر نمایان خواهد شد.

نکته بخش Startup  را از فایل App.Xaml خذف نمایید و در متد Startup این فایل کد زیر را کپی کنید:

public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            new Bootstrapper.AppBootstrapper().Run();
        }
    }
در پایان، ساختار پروژه به صورت زیر خواهد شد:

نکته: می‌توان بخش اسکن اسمبلی‌ها را توسط یک DirecotryCatalog به صورت زیر خلاصه کرد:
Catalog.Catalogs.Add(new DirectoryCatalog(Environment.CurrentDirectory));
در این صورت تمام اسمبلی‌های موجود در این مسیر اسکن خواهند شد.
نکته: می‌توان به جای جداسازی فیزیکی لایه‌ها آن‌ها را از طریق Directory‌ها به صورت منطقی در قالب یک اسمبلی نیز مدیریت کرد.
نکته: بهتر است به جای رفرنس مستقیم اسمبلی‌ها به Bootstrapper با استفاده از Pre post build در قسمت  Build Event، اسمبلی‌های مورد نظر را در یک مسیر Build کپی نمایید که روش آن به تفصیل در این پست و این پست شرح داده شده است.
دانلود سورس پروژه
مطالب
آشنایی با الگوی طراحی Builder
سناریوی زیر را در نظر بگیرید:
از شما خواسته شده است تا نحوه‌ی ساخت تلفن همراه را پیاده سازی نمایید. شما در گام اول 2 نوع تلفن همراه را شناسایی نموده‌اید (Android و Windows Phone). پس از شناسایی، احتمالا هر کدام از این انواع را یک کلاس در نظر می‌گیرید و به کمک یک واسط یا کلاس انتزاعی، شروع به ساخت کلاس می‌نمایید، تا در آینده اگر تلفن همراه جدیدی شناسایی شد، راحت‌تر بتوان آن را در پیاده سازی دخیل نمود.
اگر چنین فکر کرده اید باید گفت که 90% با الگوی طراحی Builder آشنا هستید و از آن نیز استفاده می‌کنید؛ بدون اینکه متوجه باشید از این الگو استفاده کرده‌اید. در کدهای زیر این الگو را قدم به قدم بررسی خواهیم نمود.
قدم 1: تلفن همراه چه بخش هایی می‌تواند داشته باشد؟ (برای مثال یک OS دارند، یک Name دارند و یک Screen) همچنین برای اینکه تلفن همراهی بتواند ساخته شود ابتدا بایستی نام آن‌را بدانیم. کدهای زیر همین رویه را تصدیق می‌نمایند:
public class Product
{
        public Product(string name)
        {
            Name = name;
        }
        public string Name { get; set; }
        public string Screen { get; set; }
        public string OS { get; set; }
        public override string ToString()
        {
            return string.Format(Screen + "/" + OS + "/" + Name);
        }
}
یک کلاس ساخته‌ایم و نام آن را Product گذاشتیم. بخش‌های مختلفی را نیز در آن تعریف نموده‌ایم. تابع ToString را برای استفاده‌های بعدی override کرده‌ایم (فعلا نیازی بدان نداریم).
قدم 2: برای ساخت تلفن همراه چه کارهایی باید انجام شود؟ (برای مثال بایستی OS روی آن نصب شود، Screen آن مشخص شود. همچنین بایستی به طریقی بتوانم تلفن همراه ساخته شده‌ی خود را نیز پیدا کنم). کدهای زیر همین رویه را تصدیق می‌نمایند:
    public interface IBuilder
    {
        void BuildScreen();
        void BuildOS();
        Product Product { get; }
    }
یک واسط تعریف کرده‌ایم تا به کمک آن هر تلفن همراهی را که خواستیم بسازیم.
قدم 3: از آنجا که فقط دو نوع تلفن همراه را فعلا شناسایی کرده‌ایم (Android و Windows Phone) نیاز داریم تا این دو تا را بسازیم.
ابتدا تلفن همراه Android را می‌سازیم:
  public class ConcreteBuilder1 : IBuilder
    {
        public Product p;
        public ConcreteBuilder1()
        {
            p = new Product("Android Cell Phone");
        }
        public void BuildScreen()
        {
            p.Screen = "Touch Screen 16 Inch!";
        }

        public void BuildOS()
        {
            p.OS = "Android 4.4";
        }
        public Product Product
        {
            get { return p; }
        }
    }
سپس تلفن همراه Windows Phone را می‌سازیم:
    public class ConcreteBuilder2 : IBuilder
    {
        public Product p;

        public ConcreteBuilder2()
        {
            p = new Product("Windows Phone");
        }
        public void BuildScreen()
        {
            p.Screen = "Touch Screen 32 Inch!";
        }

        public void BuildOS()
        {
            p.OS = "Windows Phone 2014";
        }
        public Product Product
        {
            get { return p; }
        }
    }
قدم 4: اول باید OS نصب شود یا Screen مشخص شود؟ برای اینکه توالی کار را مشخص سازم نیاز به یک کلاس دیگر دارم تا اینکار را انجام دهد:
    public class Director
    {
        public void Construct(IBuilder builder)
        {
            builder.BuildScreen();
            builder.BuildOS();
        }
    }
این کلاس در متد Construct خود یک ورودی از نوع IBuilder می‌گیرد و براساس توالی مورد نظر، شروع به ساخت آن می‌کند.
قدم 5: نهایتا میخواهم به برنامه‌ی خود بگویم که تلفن همراه Android را بسازد:
Director d = new Director();
ConcreteBuilder1 cb1 = new ConcreteBuilder1();
d.Construct(cb1);
Console.WriteLine(cb1.p.ToString());
و به این صورت تلفن همراه من آماده است!
متد ToString در اینجا، همان ToString ابتدای بحث است که آن را  Override کردیم.
به این نکته توجه کنید که اگر یک تلفن همراه جدید شناسایی شود، چه مقدار تغییری در کدها نیاز دارید؟ برای مثال تلفن همراه BlackBerry شناسایی شده‌است. تنها کاری که لازم است این است که یک کلاس بصورت زیر ساخته شود:
    public class BlackBerry: IBuilder
    {
        public Product p;

        public BlackBerry ()
        {
            p = new Product("BlackBerry");
        }
        public void BuildScreen()
        {
            p.Screen = "Touch Screen 8 Inch!";
        }

        public void BuildOS()
        {
            p.OS = "BlackBerry XXX";
        }
        public Product Product
        {
            get { return p; }
        }
    }
نظرات مطالب
C# 6 - The nameof Operator
یک نکته‌ی تکمیلی: تکامل اپراتور nameof در C# 12.0

همانطور که در این مطلب مشاهده کردید، اپراتور nameof، روشی بسیار مفید جهت دسترسی به نام متغیرها، نوع‌ها و یا اعضای یک کلاس است. در C# 12، این ویژگی اندکی بهبود یافته‌است و امکان دسترسی به اطلاعات اعضای یک کلاس را هم دارد:
public class NameofClass
{
    public string SomeProperty { get; set; }

    // Now legal with C# 12
    // would show "Length" on the console
    public const string NameOfSomePropertyLength = nameof(SomeProperty.Length); 
    
    public static int StaticField;
    public const string NameOfStaticFieldMinValue  = nameof(StaticField.MinValue);

    [Description($"String {nameof(SomeProperty.Length)}")]
    public int StringLength(string s)
    {
        return s.Length;
    }
}
در این مثال، اگر سعی کنیم مقدار NameOfSomePropertyLength را در کنسول نمایش دهیم، عبارت Length ظاهر خواهد شد. تا پیش از C# 12 برای دسترسی به یک چنین قابلیتی نیاز به نمونه سازی و تولید شیءای از کلاس NameofClass فوق وجود داشت تا بتوان اپراتور nameof را به خواص آن اعمال کرد. این محدودیت در C# 12 برطرف شده‌است.

همچنین همانطور که مشاهده می‌کنید، امکان دسترسی به اطلاعات فیلدهای استاتیک و یا بکارگیری این قابلیت در Attributes هم میسر شده‌است.
مطالب
تزریق وابستگی‌ها در پروفایل‌های AutoMapper در برنامه‌های ASP.NET Core
Profileهای AutoMapper، قابلیت تزریق وابستگی‌ها را در سازنده‌ی خود ندارند؛ به همین جهت در این مطلب، دو راه حل را جهت رفع این محدودیت بررسی می‌کنیم.


مثال: نیاز به نگاشت کلمه‌ی عبور، به کلمه‌ی عبور هش شده

فرض کنید موجودیت کاربری که قرار است در بانک اطلاعاتی ذخیره شود، چنین ساختاری را دارد:
namespace AutoMapperInjection.Entities
{
    public class User
    {
        public int Id { set; get; }

        public string HashedPassword { set; get; }
    }
}
در اینجا کلمه‌ی عبور، به صورت معمولی ذخیره نمی‌شود و معادل هش شده‌ی آن ذخیره خواهد شد. اما اطلاعاتی را که از کاربر دریافت می‌کنیم:
namespace AutoMapperInjection.Models
{
    public class UserDto
    {
        public string Password { set; get; }
    }
}
حاوی کلمه‌ی عبور معمولی است که باید در حین نگاشت UserDto به User، هش شود. این اطلاعات نیز به صورت زیر توسط اکشن متد RegisterUser دریافت می‌شوند که توسط متد mapper.Map، قرار است به یک نمونه از شیء User تبدیل شود:
namespace AutoMapperInjection.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        private readonly IMapper _mapper;

        public HomeController(IMapper mapper)
        {
            _mapper = mapper ?? throw new NullReferenceException(nameof(mapper));
        }

        [HttpPost("[action]")]
        public IActionResult RegisterUser(UserDto model)
        {
            var user = _mapper.Map<User>(model);

            // TODO: Save user

            return Ok();
        }
    }
}
کار این هش شدن نیز برای مثال توسط سرویس زیر انجام می‌شود:
using System;
using System.Security.Cryptography;
using System.Text;

namespace AutoMapperInjection.Services
{
    public interface IPasswordHasherService
    {
        string GetSha256Hash(string input);
    }

    public class PasswordHasherService : IPasswordHasherService
    {
        public string GetSha256Hash(string input)
        {
            using var hashAlgorithm = new SHA256CryptoServiceProvider();
            var byteValue = Encoding.UTF8.GetBytes(input);
            var byteHash = hashAlgorithm.ComputeHash(byteValue);
            return Convert.ToBase64String(byteHash);
        }
    }
}
به همین جهت در حین تعریف نگاشت‌های AutoMaper، نیاز خواهیم داشت تا بتوانیم از این سرویس استفاده کنیم.


روش اول: IValueResolver‌ها قابلیت تزریق وابستگی را در سازنده‌ی خود دارند

توسط یک IValueResolver سفارشی، می‌توانیم مشخص کنیم که برای مثال اطلاعات یک خاصیت خاص، چگونه باید از منبع دریافتی تامین شود:
        public class HashedPasswordResolver : IValueResolver<UserDto, User, string>
        {
            private readonly IPasswordHasherService _hasher;

            public HashedPasswordResolver(IPasswordHasherService hasher)
            {
                _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
            }

            public string Resolve(UserDto source, User destination, string destMember, ResolutionContext context)
            {
                return _hasher.GetSha256Hash(source.Password);
            }
        }
همانطور که مشاهده می‌کنید، در اینجا می‌توان سرویس مدنظر را به سازنده‌ی این کلاس تزریق کرد و سپس از آن جهت تامین مقدار هش شده‌ی کلمه‌ی عبور استفاده کرد. IValueResolverها تنها تامین کننده‌ی مقدار یک خاصیت، در حین نگاشت هستند.

پس از آن، روش استفاده‌ی از این تامین کننده‌ی مقدار سفارشی، به صورت زیر است:
    public class UserDtoMappingsProfile : Profile
    {
        public UserDtoMappingsProfile()
        {
            // Map from User (entity) to UserDto, and back
            this.CreateMap<User, UserDto>()
                .ReverseMap()
                .ForMember(user => user.HashedPassword, exp => exp.MapFrom<HashedPasswordResolver>());
        }
    }
با این تعاریف، هر زمانیکه قرار است کار var user = _mapper.Map<User>(model) انجام شود، مقدار خاصیت HashedPassword، از طریق HashedPasswordResolver تامین می‌شود که در اینجا کار تزریق وابستگی‌های آن نیز به صورت خودکار توسط AutoMapper مدیریت می‌شود. البته بدیهی است که سرویس IPasswordHasherService باید به نحو زیر پیشتر به سیستم معرفی شده باشد:
namespace AutoMapperInjection
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IPasswordHasherService, PasswordHasherService>();
            services.AddAutoMapper(typeof(UserDtoMappingsProfile).Assembly);
            services.AddControllers();
        }


روش دوم: IMappingAction‌ها قابلیت تزریق وابستگی را در سازنده‌ی خود دارند

می‌توان پیش و یا پس از عملیات نگاشت، منطقی را توسط یک IMappingAction سفارشی بر روی آن اجرا کرد که در اینجا نیز امکان تزریق وابستگی در سازنده‌ی IMappingActionهای پیاده سازی شده، وجود دارد:
        public class UserDtoMappingsAction : IMappingAction<UserDto, User>
        {
            private readonly IPasswordHasherService _hasher;

            public UserDtoMappingsAction(IPasswordHasherService hasher)
            {
                _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
            }

            public void Process(UserDto source, User destination, ResolutionContext context)
            {
                destination.HashedPassword = _hasher.GetSha256Hash(source.Password);
            }
        }
در اینجا می‌خواهیم مقدار یک یا چندین خاصیت از مقصد نگاشت را (شیء User در اینجا) پس از نگاشت ابتدایی از طریق مقدار دریافتی از کاربر، با منطق خاصی تغییر دهیم. برای مثال کلمه‌ی عبور ساده‌ی دریافتی را هش کنیم و به خاصیت خاصی نسبت دهیم (و یا حتی مقدار خواص دیگری را نیز پس از نگاشت، تغییر دهیم).

در این حالت روش استفاده‌ی از کلاس UserDtoMappingsAction به صورت زیر است:
    public class UserDtoMappingsProfile : Profile
    {
        public UserDtoMappingsProfile()
        {
            // Map from User (entity) to UserDto, and back
            this.CreateMap<User, UserDto>()
                .ReverseMap()
                .AfterMap<UserDtoMappingsAction>();
        }
    }


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: AutoMapperInjection.zip
مطالب
مقدمه ای بر AutoMapper
AutoMapper کتابخانه ای ساده و سبک برای نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار هست و...
اگه این پست رو مطالعه کرده باشید یه مشکل امنیتی بنام «Mass Assignment» مطرح شد.برای رفع این مشکل یک روش استفاده از ViewModel بود.
فرض کنید Model ما
 public class User
    {
        public int Id { get; set; }
       
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string UserName { get; set; }

        public string Password { get; set; }

        public bool IsAdmin { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; }
    }
و ViewModel ما
 public class UserViewModel
    {
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string Password { get; set; }
    }
باشه(توجه کنید در واقع من برای View ی مورد نظرم فقط به نام , نام خانوادگی و پسورد نیاز دارم)
برای استفاده UserViewModel بعنوان Model در View ی مورد نظر باید شی UserViewModel رو با اطلاعات شی User مقدار دهی کنیم مثلا با کدی مثل این در کنترلر.
 
public ActionResult Index(int id = 1)
        {
            var user = _userService.GetById(id);
            var userViewModel = new UserViewModel
                {
                    FirstName = user.FirstName,
                    LastName = user.LastName,
                    Password = user.Password
                };

            return View(userViewModel);
        }
رهایی از نوشتن اینجور کدهای تکراری و خسته کننده باعث پیدایش AutoMapper شد...
برای استفاده از AutoMapper از نوگت استفاده میکنیم.
PM> Install-Package AutoMapper
در شروع برنامه نگاشت‌ها رو تعریف میکنم.یک روش ابداعی تعریف نگاشت‌ها در یک کلاس استاتیک و فراخوانی اون تو متد  Application_Start هست.
public static class AutoMapperWebConfiguration
    {

        public static void Configure()
        {
            ConfigureUserMapping();
        }

        private static void ConfigureUserMapping()
        {
            Mapper.CreateMap<User, UserViewModel>();
        }
    }

اولین پارامتر نوع مبدا و دومین پارامتر نوع مقصد هست.
برای انجام نگاشت هم از متد Map استفاده میکنیم.
public ActionResult Index(int id=1)
        {
            var user = _userService.GetById(id);
            var userViewModel=new UserViewModel();
            AutoMapper.Mapper.Map(user, userViewModel);
            
            return View(userViewModel);
        }
همنطور که میبنید با نوشتن چندین خط کد عملیات نگاشت اطلاعات یک شی به شی دیگه انجام شد.
ادامه دارد...
مطالب
ویژگی های کمتر استفاده شده در NET. - بخش ششم

#Execute VB code via C

می توان از طریق #C، ماکروهای Visual Basic مورد استفاده‌ی در Office را تولید کرد.
static void AddChartButton( Workbook workBook,
                            Worksheet xlWorkSheetNew,
                            Range currentRange,  int macroId,
                            int startRow, int endRow,
                            int startCol, int endCol,
                            string buttonImagePath )
{
    var cell = currentRange.Next;
    var width = cell.Width;
    var height = 24;
    var left = cell.Left;
    var top = System.Math.Max( cell.Top + cell.Height - height, 0 );
    var button = xlWorkSheetNew.Shapes.AddPicture( buttonImagePath,
                                                    MsoTriState.msoFalse,
                                                    MsoTriState.msoCTrue,
                                                    left, top, width, height );
    var module = workBook.VBProject.VBComponents.Add( vbext_ComponentType.vbext_ct_StdModule );
    module.CodeModule.AddFromString( GetMacro( macroId,
                                                startRow, endRow,
                                                startCol, endCol ) );
    button.OnAction = "Macro" + macroId;
}

static string GetMacro( int macroId,
                        int startRow,  int endRow,
                        int startCol, int endCol )
{
    var sb = new StringBuilder();
    var range = "ActiveSheet.Range(Cells(" + startRow + "," + startCol + "), Cells(" + endRow + "," + endCol + ")).Select";
    sb.AppendLine( "Sub Macro" + macroId + "()" );
    sb.AppendLine( "On Error Resume Next" );
    sb.AppendLine( range );
    sb.AppendLine( "ActiveSheet.Shapes.AddChart.Select" );
    sb.AppendLine( "ActiveChart.ChartType = xlColumn" );
    sb.AppendLine( "ActiveChart.SetSourceData Source:=" + range );
    sb.AppendLine( "On Error GoTo 0" );
    sb.AppendLine( "End Sub" );
    return sb.ToString();
}

و برای استفاده از آن می‌توان مانند مثال زیر عمل کرد:

var excelApp = new Microsoft.Office.Interop.Excel.Application();
var fileName = @"C:\Users\Vahid\Desktop\VBA.xlsm";
var workBook = excelApp.Workbooks.Open( fileName );
var sheet = workBook.Sheets[1];

AddChartButton( workBook,
                sheet,
                sheet.Range["B1"],
                1231,
                1,
                10,
                1,
                2,
                @"C:\Users\Vahid\Desktop\BarChart.png");

excelApp.DisplayAlerts = false;
workBook.Close( true,
                fileName );

خروجی مثال بالا


نکته: در صورتیکه بعد از اجرای برنامه، خطای " programmatic access to visual basic project is not trusted"  رخ داد از این طریق می‌توانید مشکل را حل کنید.

File -> Options -> Trust Center -> Trust Center Settings -> Macro Settings -> Trust Access to the VBA Project object model


volatile

کلمه کلیدی volatile نشان می‌دهد که یک فیلد ممکن است توسط چندین thread به صورت همزمان تغییر کند. فیلدهایی که به عنوان volatile تعریف می‌شوند، شامل بهینه سازی کامپایلر برای دسترسی از طریق تنها یک thread قرار نمی‌گیرند و بروزرسانی مقدار فعلی این فیلد را در تمامی زمان‌ها، تضمین می‌کند.
class Program
{
    volatile bool _shouldPartyContinue = true;

    static void Main()
    {
        var firstDimension = new Program();
        var secondDimension = new Thread( firstDimension.StartPartyInAnotherDimension );
        secondDimension.Start( firstDimension );
        Thread.Sleep( 5000 );
        firstDimension._shouldPartyContinue = false;
        Console.WriteLine( "Party Finish" );
    }

    void StartPartyInAnotherDimension( object input )
    {
        var currentDimensionInput = (Program)input;
        Console.WriteLine( "let the party begin" );
        while ( currentDimensionInput._shouldPartyContinue ) {}
        Console.WriteLine( "Party ends: (" );
    }
}
نکته: اگر متغیر shouldPartyContinue به وسیله volatile علامت گذاری نشده بود، برنامه در حالت Release (که گزینه Optimize code تیک داشته باشد) هیچگاه به پایان نمی‌رسید.

::global

وقتی که یک عضو توسط موجودیت دیگری با همان نام مخفی شده باشد، با استفاده از کلمه کلیدی ::global (فضای نام سراسری) قابلیت دسترسی به آن امکان پذیر می‌شود.
class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine( Number );  // Error
        global::System.Console.WriteLine( "Console: " + Console ); //OK
    }

    public class System { }

    // Define a constant called ‘Console’ to cause more problems.
    const int Console = 7;
    const int Number = 67;
}
همانطور که در مثال بالا مشاهده می‌کنید، به دلیل وجود ثابت Console و کلاس System امکان دسترسی به متد WriteLine وجود ندارد، برای در دسترس قرار گرفتن آن باید از ::global استفاده کرد.

DebuggerDisplayAttribute

با استفاده از  DebuggerDisplayAttribute  می‌توانید نحوه نمایش یک فیلد یا یک کلاس را در پنجره متغیر دیباگر مشخص کنید.
[DebuggerDisplay( "{DebuggerDisplay}" )]
public class DebuggerDisplayTest
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public int Age { get; set; }

    [DebuggerBrowsable( DebuggerBrowsableState.Never )]
    string DebuggerDisplay => $"{FirstName} {LastName} {Age} years old";
}
و بعد از استفاده‌ی از آن، خروجی زیر بدست می‌آید:

همچنین شما می‌توانید عبارات مختلفی را به صورت مستقیم در این attribute استفاده کنید.
[DebuggerDisplay( "Age {Age > 0 ? Age : 25}" )]
public class DebuggerDisplayTest
{
 //...
}
اگر مقدار پروپرتی Age بیشتر از 0 باشد، مقدار Age و در غیراینصورت 25 نشان داده می‌شود.
مطالب
شروع به کار با EF Core 1.0 - قسمت 2 - به روز رسانی ساختار بانک اطلاعاتی
پس از برپایی تنظیمات اولیه‌ی کار با EF Core در ASP.NET Core، اکنون نوبت به تبدیل کلاس Person، به جدول معادل آن در بانک اطلاعاتی برنامه است. در EF Core نیز همانند EF Code First 6.x، برای انجام یک چنین اعمالی از مفهومی به نام Migrations استفاده می‌شود که در ادامه به آن خواهیم پرداخت.


پیشنیازهای کار با EF Core Migrations

در قسمت قبل در حین بررسی «برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.


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

پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشه‌ی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته: در ویندوز اگر در پوشه‌ای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینه‌ی جدیدی را به نام Open command window here مشاهده خواهید کرد که می‌تواند به سرعت خط فرمان را از پوشه‌ی جاری شروع کند.

خروجی صدور فرمان dotnet ef را در ذیل مشاهده می‌کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef
                     _/\__
               ---==/    \\
         ___  ___   |.    \|\
        | __|| __|  |  )   \\\
        | _| | _|   \_/ |  //|\\
        |___||_|       /   \\\/\\
Entity Framework .NET Core CLI Commands 1.0.0-preview2-21431
Usage: dotnet ef [options] [command]
Options:
  -h|--help                      Show help information
  -v|--verbose                   Enable verbose output
  --version                      Show version information
  --assembly <ASSEMBLY>          The assembly file to load.
  --startup-assembly <ASSEMBLY>  The assembly file containing the startup class.
  --data-dir <DIR>               The folder used as the data directory (defaults to current working directory).
  --project-dir <DIR>            The folder used as the project directory (defaults to current working directory).
  --content-root-path <DIR>      The folder used as the content root path for the application (defaults to application base directory).
  --root-namespace <NAMESPACE>   The root namespace of the target project (defaults to the project assembly name).
Commands:
  database    Commands to manage your database
  dbcontext   Commands to manage your DbContext types
  migrations  Commands to manage your migrations
Use "dotnet ef [command] --help" for more information about a command.
در قسمت Commands آن در انتهای لیست، از فرمان migrations آن استفاده خواهیم کرد. برای این منظور در همین پوشه‌ی جاری، دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
دستورات migrations با dotnet ef migrations شروع شده و سپس یک سری پارامتر را دریافت می‌کنند برای مثال در اینجا سوئیچ add، به همراه یک نام دلخواه ذکر شده‌است (نام این مرحله را InitialDatabase گذاشته‌ایم). پس از فراخوانی این دستور، اگر به Solution explorer مراجعه کنید، پوشه‌ی جدید Migrations، قابل مشاهده است:


نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده می‌کنید.
اگر قصد حذف این مرحله را داشته باشیم، می‌توان دستور dotnet ef migrations remove را مجددا صادر کرد.

فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوه‌ی ایجاد جدول جدیدی را از کلاس Person بیان می‌کند و متد Down نحوه‌ی Drop این جدول را پیاده سازی کرده‌است.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصه‌ای از تعاریف موجودیت‌های ذکر شده‌ی در DB Context برنامه را به همراه دارد و تفسیر آن‌ها را از دیدگاه  EF در اینجا می‌توان مشاهده کرد.

پس از مرحله‌ی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کرده‌است. اما هنوز آن‌ها را به بانک اطلاعاتی برنامه اعمال نکرده‌است. برای اینکار در پوشه‌ی جاری دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
Applying migration '13950526050417_InitialDatabase'.
Done.
همانطور که ملاحظه می‌کنید، دستور dotnet ef database update سبب اعمال اطلاعات فایل 13950526050417_InitialDatabase به بانک اطلاعاتی شده‌است.
اکنون اگر به لیست بانک‌های اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person می‌توان مشاهده کرد:


در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهده‌است که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسه‌های آتی است. این جدول صرفا توسط ابزارهای EF استفاده می‌شود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.


مقدار دهی اولیه‌ی جداول بانک‌های اطلاعاتی در EF Core

در همین حالت اگر کنترلر TestDB مطرح شده‌ی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:


این تصویر بدین معنا است که کار Migrations موفقیت آمیز بوده‌است و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیه‌ای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیه‌ی جداول را می‌توان انجام داد (زمانیکه جدولی ایجاد می‌شود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشه‌ی جدید Migrations اضافه کنید؛ با این محتوا:
using System.Collections.Generic;
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class ApplicationDbContextSeedData
    {
        public static void SeedData(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                if (!context.Persons.Any())
                {
                    var persons = new List<Person>
                    {
                        new Person
                        {
                            FirstName = "Admin",
                            LastName = "User"
                        }
                    };
                    context.AddRange(persons);
                    context.SaveChanges();
                }
            }
        }
    }
}
و سپس نحوه‌ی فراخوانی آن در متد Configure کلاس آغازین برنامه به صورت زیر است:
public void Configure(IServiceScopeFactory scopeFactory)
{
    scopeFactory.SeedData();
به همراه این تغییر در نحوه‌ی معرفی Db Context برنامه:
public void ConfigureServices(IServiceCollection services)
{
   services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
توضیحات:
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آن‌را به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت می‌کند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شده‌است. همچنین در این مثال از نمونه‌ای که IConfigurationRoot به سازنده‌ی کلاس ApplicationDbContext تزریق شده (نکته‌ی انتهای بحث قسمت قبل)، استفاده شده‌است. به همین جهت تنظیمات options آن‌را ملاحظه نمی‌کنید.
- مرحله‌ی بعد نحوه‌ی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگی‌ها نداریم و باید کار را با  IServiceScopeFactory شروع کنیم. در اینجا می‌توانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویس‌های توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار می‌گیرد.
- نکته‌ی مهمی که در اینجا بکار رفته‌است، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواست‌ها توسط ASP.NET Core انجام می‌شود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آن‌را به صورت دستی انجام دهیم که نمونه‌ای از آن‌را در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه می‌کنید. در اینجا Scope ایی ایجاد شده‌است. سپس داده‌های اولیه‌ی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شده‌است.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شده‌است انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواست‌ها نرسیده‌است. بنابراین مدیریت Scopeها هنوز شروع نشده‌است و باید به صورت دستی انجام شود.

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


که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیه‌ای در بانک اطلاعاتی است.


اعمال تغییرات به مدل‌های برنامه و به روز رسانی ساختار بانک اطلاعاتی

فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کرده‌ایم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public int Age { get; set; }
    }
}
در این حالت اگر برنامه را اجرا کنیم، به استثنای ذیل خواهیم رسید:
 An unhandled exception occurred while processing the request.
SqlException: Invalid column name 'Age'.
برای رفع این مشکل نیاز است مجددا مراحل Migrations را اجرا کرد:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add v2
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
در اینجا همان دستورات قبل را مجددا اجرا می‌کنیم. با این تفاوت که اینبار نام دلخواه این مرحله را مثلا v2، به معنای نگارش دوم وارد کرده‌ایم.
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشه‌ی Migrations اضافه می‌شود. این فایل حاوی نحوه‌ی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحله‌ی v2 انجام می‌شود.


بنابراین هر بار که تغییری را در مدل‌های خود ایجاد می‌کنید، یکبار باید کلاس مهاجرت آن‌را ایجاد کنید و سپس آن‌را به بانک اطلاعاتی اعمال نمائید.


تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF

شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار می‌توانید دستور ذیل را در پوشه‌ی جاری پروژه اجرا کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations script -o v2.sql
در این حالت اسکریپت SQL تغییرات، در فایلی به نام v2.sql، در ریشه‌ی جاری پروژه تولید می‌شود.


تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0


در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچه‌ی تغییرات، بسیار پیچیده بود و شامل رشته‌ای gzip شده‌ی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحله‌ی migration بود. در این نگارش، این snapshot حذف شده‌است و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده می‌کنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شده‌اند و دیگر خبری از رشته‌ی gzip شده‌ی قبلی نیست (تصویر فوق).

در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده می‌کنید. در EF 6.x حتی فضای نام کلاس‌های موجودیت‌های برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد می‌شود:



مهاجرت خودکار از EF Core حذف شده‌است

در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
public Configuration()
{
   AutomaticMigrationsEnabled = true;
}
کار آن مهاجرت خودکار اطلاعات context به بانک اطلاعاتی بود؛ بدون نیازی به استفاده از دستورات خط فرمان مرتبط. تمام این موارد از EF Core حذف شده‌اند و علت آن‌را می‌توانید در توضیحات یکی از اعضای تیم EF Core در اینجا مطالعه کنید و خلاصه‌ی آن به این شرح است:
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچه‌ی مهاجرت‌های فوق).
- در حالت افزودن یک مرحله‌ی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- می‌توان چندین مرحله‌ی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آن‌ها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیم‌ها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدل‌ها در جدول __EFMigrationsHistory ذخیره نمی‌شود.

بنابراین در EF Core می‌توان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای این‌ها را به بانک اطلاعاتی انجام داد.

هرچند در اینجا اگر می‌خواهید مرحله‌ی اجرای دستور dotnet ef database update را حذف کنید، می‌توانید از کدهای ذیل نیز استفاده نمائید:
using Core1RtmEmptyTest.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class DbInitialization
    {
        public static void Initialize(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                // Applies any pending migrations for the context to the database.
                // Will create the database if it does not already exist.
                context.Database.Migrate();
            }
        }
    }
}
روش فراخوانی آن نیز همانند روش فراخوانی متد SeedData است که پیشتر بحث شد.
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشده‌اند (دقیقا همان کار دستور dotnet ef database update را انجام می‌دهد).


تفاوت متد Database.EnsureCreated با متد Database.Migrate

اگر به متدهای context.Database دقت کنید، یکی از آن‌ها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه می‌شود. اما هدف آن صرفا استفاده‌ی از آن در آزمون‌های واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمی‌کند (برخلاف متد Migrate). بنابراین بجز آزمون‌های واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفاده‌ی از بانک اطلاعاتی تولید شده‌ی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز می‌توان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.



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