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

 در این بخش تعاریف، خصوصیات، مزایا و معایب مرتبط با این نوع سیستم‌ها بررسی می‌شوند.


تعریف سیستمهای توزیع شده

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


تعاریف سیستم‌های توزیع شده

تعریف اول: سیستمهای توزیع  شده از مجموعه‌ای از سخت افزارها و نرم افزارها که برای رسیدن به یک هدف واحد از طریق شبکه با یکدیگر در ارتباطند تشکیل شده.
تعریف دوم: یک مجموعه از Computerهای مستقل که از نظر کاربر یک Computer و یک سیستم واحد و منسجمند.
تعریف سوم: سیستم‌های توزیع شده در واقع هنری هستند که بوسیله آنها می‌توانید مشکلاتی را که در یک کامپیوتر وجود دارند، با استفاده از چند کامپیوتر رفع کنید و معمولا به این دلیل انتخاب می‌شوند که این مشکلات توسط یک کامپیوتر رفع نمی‌شوند.
تعریف چهارم: زیر سیستمهایی که بصورت همزمان می‌توانند پردازش یک سیستم بزرگ را انجام دهند؛ البته همزمانی که در آن مانند سیستمهای Parallel از یک حافظه مشترک استفاده نشود.
تعریف پنجم: مجموعه ای از پردازشهای مرتبط به هم، بصورتی که هر پردازش از حافظه داخلی مرتبط با خودش استفاده کند و تمام این پردازش‌ها از طریق Message passing در سطح شبکه با یکدیگر در ارتباطند.
تعریف ششم: تقسیم بندی وظایف یک سیستم بزرگ به زیرسیستمهایی که در سخت افزارهای مجزا اجرا می‌شوند و کاربر هیچ تصوری از وجود این زیر سیستمها ندارد.

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


نکات کلیدی یا خصوصیات سیستمهای توزیع شده

1- داشتن یک هدف واحد در سیستم‌های توزیع شده، زیر سیستمهایی که از نظر کاربر یک سیستم واحد و متمرکزند:  این یکی از خصوصیات و نکات کلیدی سیستم‌های توزیع شده‌است که تمام اجزاء و زیرسیستم‌ها در راه رسیدن به یک هدف واحد با یکدیگر در ارتباط اند. ارتباطی که باعث همکاری آنها می‌شود و همکاری که باید بصورت کامل از دید کاربر مخفی بماند (داشتن یک هدف واحد یکی از تفاوتهای سیستم‌های توزیع شده با Cloud distributed systemها می‌باشد. چون در Cloud distributed systemها لزوما تمام اجزا برای رسیدن به یک هدف مشترک با یکدیگر کار نمی‌کنند).

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

3- تقسیم بندی وظایف: که یکی از نکات کلیدی این نوع سیستمها می‌باشد. تقسیم بندی می‌تواند به هر دلیلی که شما درنظر می‌گیرید صورت بپذیرد. دلایلی برای بالا بردن کارآیی، امنیت، در دسترس بودن، یا حتی دلایل مربوط به Business سیستم شما.

4- ارتباط از طریق شبکه، ارتباطی که از طریق Message passing صورت می‌پذیرد: این خصوصیت در واقع پایه تمام تعاریف سیستم‌های توزیع شده‌است. در سیستم‌های توزیع شده همه چیز از ارتباط‌هایی که از طریق شبکه صورت می‌پذیرد، شروع می‌شود .


مزایای استفاده از سیستم‌های توزیع شده

1- کارآیی بسیار بالاتر: بدلیل همزمان اجرا شدن کارها در سخت افزارهای مختلف، کارآیی این نوع سیستم‌ها بسیار بیشتر از سیستم‌های متمرکز است.
2- قابلیت همکاری بیشتر: بدلیل اینکه این سیستمها ذاتا توزیع شده هستند، با کمترین هزینه و پیچیدگی می‌توانند با سایر سیستمها همکاری لازم را داشته باشند.
3- قابلیت در دسترس بودن و اطمینان بیشتر: در این سیستم‌ها با روشهای مختلفی مانند replication، به راحتی می‌توان این دو قابلیت را در بالاترین سطح قرار داد.
4- مقیاس پذیری: مقیاس پذیری در این سیستمها با قرار دادن کامپوننت‌هایی که قابلیت استفاده مجدد بالایی را دارند در سرور‌های جدید به راحتی و بدون از دسترس خارج شدن سیستم صورت می‌پذیرد.
5- قابلیت گسترش: برای گسترش سیستم و اضافه کردن نیازمندی‌های جدید در این سیستم‌ها، به راحتی می‌توان کامپوننت‌ها و زیرسیستمهای جدیدی را پیاده سازی کرد و بدون از دسترس خارج شدن سیستم و به گونه‌ای که به راحتی با سایر قسمت‌های موجود در ارتباط باشند، آنها را به سیستم اضافه کرد.
6- بهره وری بالاتر و زمان توسعه کمتر: بدلیل تقسیم بندی قسمتهای بزرگ به قسمتهای کوچکتر، تیمهای مختلف می‌توانند بصورت همزمان قسمتهای کوچک را توسعه دهند.
7- قابلیت استفاده مجدد بسیار بالا: در این نوع سیستم‌ها به راحتی می‌توانید از یک زیرسیستم یا کامپوننت خاص که یکبار پیاده سازی شده و در سخت افزار جداگانه‌ای اجرا شده، در تمام Application‌ها استفاده کنید.
8- کاهش هزینه: در مواردی مانند قابلیت استفاده‌ی مجدد بالا و توسعه پذیری سیستم، می‌توانند باعث کاهش هزینه‌ها شوند (در صورت انتخاب نادرست این نوع سیستم، این مزیت می‌تواند تبدیل به یکی از معایب سیستم شود).
9- امنیت: بدلیل اینکه هر زیرسیستم در یک سخت افزار جداگانه اجرا می‌شود که مکان آن از قبل مشخص نیست و همچنین تقسیم بندی قسمت‌هایی که نیاز به امنیت بالایی دارند، می‌تواند بر اساس نیاز و در سخت افزارهایی که حتی به اینترنت هم متصل نیستند، صورت بپذیرد. این نوع سیستم می‌تواند از امنیت بالایی برخوردار باشد (البته در صورت طراحی نادرست، امنیت می‌تواند بعنوان یکی از معایب این نوع سیستمها نیز مطرح شود).


معایب  استفاده از سیستم‌های توزیع شده

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

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

3- هزینه طراحی و پیاده سازی بیشتر: دلایل 1 و 2 می‌توانند باعث بالا رفتن هزینه‌های طراحی و پیاده سازی این نوع سیستم‌ها شوند.

4- هزینه‌های بیشتر مرتبط با شبکه: در این نوع سیستم‌ها بدلیل استفاده بیشتر از منابع، مانند سخت افزار‌ها و ابزارهای مرتبط با شبکه، هزینه‌های مرتبط با استفاده از منابع، مانند برق و شبکه بیشتر از سیستم‌های متمرکز است.

5- کاهش امنیت: توزیع نادرست منابع سیستم در سخت افزارهای متفاوت و مدیریت نادرست این منابع باعث کاهش امنیت این نوع سیستم‌ها می‌شود.

6- مدیریت دشوارتر: بدلیل اینکه سیستمهای توزیع شده از زیرسیستم‌های زیادی تشکیل شده و هر یک از این زیر سیستمها در سخت افزارهای متفاوتی اجرا شده‌اند، مدیریت و سازماندهی این نوع سیستم‌ها دشوار‌تر از سیستم‌های متمرکز است.

بدلیل اینکه همیشه قسمتهای مرتبط با پیاده سازی این نوع سیستم‌ها از تعاریف آنها جذابتر است، سعی کرده‌ام در این بخش و بخش بعد که اهداف و اصطلاحات مرتبط با سیستمهای توزیع شده را بررسی میکند، کمتر وارد جزئیات مفاهیم شوم. در صورت نیاز به توضیح بیشتر در مورد قسمت خاصی از این مفاهیم، در قسمت نظرات آن‌ها را مطرح کنید.
مطالب
ایجاد چارت سازمانی تحت وب #3
در مطالب قسمت اول و دوم به نحوه ایجاد و تغییر رنگ چارت سازمانی اشاره شد. در این مطلب ، نحوه تغییر فونت‌ها، مکان قرار گرفتن شاخه‌ها و ایجاد لینک در شاخه‌ها ارائه میشود. بدین صورت که در شکل زیر مشاهده مینمائید:

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

با توجه به محدودیت IE در پیاده سازی excanvas  ، در کل کاراکترها متن نود کوتاه میشود. ( اگر کاراکترهای نود ، کاملا پرشونده fit نشوند ، بخشی از کل متن کاراکترهای نود نوشته یا رسم خواهد شد )

پارامترهای تابع ()setFont :

  1. نام فونت . حالت فونت ضخیم bold  یا مورب italic قابل استفاده است.
  2. اندازه فونت در واحد پیکسل
  3. رنگ فونت ( اختیاری )
  4. چیدمان عمودی ( 1 و c یا center برای وسط چین . ( اختیاری )
برای مشاهده از این کدها میتوانید استفاده نمائید:
var o = new orgChart();

o.setColor('#99CC99', '#CCFFCC');
o.setFont('arial', 18);
o.addNode(0, '', '', 'Arial 18', 1);

o.setColor('#CCCC66', '#FFFF99');
o.setFont('arial', 10, '#000000');
o.addNode(11, 0, 'u', 'text will be split if it does not fit. ThisIsAVeryLongWordAndItWillBeClipped. Too many lines will be clipped too.');

o.setFont('arial', 10, '#000000', 0);
o.addNode(12, 0, 'u', 'aligned at top');

o.setFont('arial', 10, '#000000', 1);
o.addNode(13, 0, 'u', 'centered');
o.setColor('#CC4950', '#FF7C80');

o.setFont('times', 16, '#FF0F00');
o.addNode(21, 0, 'l', 'Times 16 red');

o.setFont('times', 16, '#000000');
o.addNode(22, 0, 'l', 'Times 16');

o.setFont('times', 48, '#000000');
o.addNode(23, 0, 'l', 'Times 48 does not fit at all');
o.setColor('#CC9966', '#FFCC99');

o.setFont('jokerman', 12, '#000000');
o.addNode(31, 0, 'r', 'Jokerman (if available)');

o.setFont('bold times', 16, '#000000');
o.addNode(32, 0, 'r', 'bold Times 16');

o.setFont('italic times', 16, '#000000');
o.addNode(33, 0, 'r', 'italic Times 16');

o.setFont('bold italic times', 16, '#000000');
o.addNode(34, 0, 'r', 'bold italic Times 16');

o.setColor('#B5D9EA', '#CFE8EF');
o.setFont('arial', 28, '#000000');
o.addNode(50, '', '', 'Arial 28');
o.setFont('arial', 29);
o.addNode(51, 50, 'u', 'Arial 29');
o.setFont('arial', 30);
o.addNode(52, 51, 'u', 'Arial 30');
o.setFont('arial', 31);
o.addNode(53, 52, 'u', 'Arial 31');
o.setFont('arial', 32);
o.addNode(54, 53, 'u', 'Arial 32');

o.drawChart('c_fonts');

اندازه و مکان :

شما میتوانید اندازه نودها و فضا و offset بین نودها را نیز تنظیم نمائید.این تنظیم بصورت عمومی تاثیر گذار است و تمامی نودها از این تنظیم تبعیت خواهند نمود:


پارامترهای تابع ()setSize:
  1. عرض نودها در واحد پیکسل.
  2. ارتفاع نودها در واحد پیکسل.
  3. فاصله عرضی بین نودهای پدر u-nodes. ( اختیاری ).
  4. فاصله طولی بین نودها ( اختیاری ).
  5. offset عرضی ( فاصله )  از نود چپ و نود راست ( اختیاری ).
var o = new orgChart();

o.setSize(36, 12, 4, 25, 180);

o.setColor('#99CC99', '#CCFFCC');
o.setFont('arial', 18);
o.addNode(0, '', '', 'Root node');
o.setFont('arial', 12);
o.setColor('#CCCC66', '#FFFF99');
o.addNode(11, 0, 'u', 'u-node 1');
o.addNode(12, 0, 'u', 'u-node 2');
o.addNode(13, 0, 'u', 'u-node 3');
o.setColor('#CC4950', '#FF7C80');
o.addNode(21, 0, 'l', 'l-node 1');
o.addNode(22, 0, 'l', 'l-node 2');
o.addNode(23, 0, 'l', 'l-node 3');
o.setColor('#CC9966', '#FFCC99');
o.addNode(31, 0, 'r', 'r-node 1');

o.drawChart('c_size');
پیوندها: LINKS
شما میتوانید به نودها در پارامتر ششم تابع ()addNode آدرس پیوند خود را اضافه نمائید.
در صورت ایجاد پیوند کامل ( مانند : http://www.yourdomain.com ) پیوند در برگه ( tab ) یا یک پنجره جدید ( بسته به تنظیمات مرورگر سمت کاربر ) باز خواهد شد.
اگر نشانگر ماوس ، روی این نوع از نود‌ها قرار بگیرد تغییر شکل به مانند دست ( اشاره گر ) میدهد.



نکته : در این نمونه کد ، هر نود در یک چارت سازمانی جدید دوباره رسم شده اند.در چارت سازمانی قدیمی ، نودها از بین نمی‌روند و همه مسیرهای باقی مانده فعال خواهند ماند.بنابراین اگر reDraw در این نمونه استفاده شود ، چند پیوند در یک نود باز خواهد شد .
اگر بخواهید فقط یک لینک به نودی اختصاص دهید ، یک نود پیوندی بدون پیوند به آن اضافه کنید ( مانند نودها سبز مثال نمونه ).
var o = new orgChart();

o.setColor('#99CC99', '#CCFFCC');
o.setFont('arial', 18);
o.addNode( 0, '', '', 'Searching', 1);
o.addNode(50, '', '', 'Social', 1);
o.addNode(90, '', '', 'Misc.', 1);

o.setColor('#CCCC66', '#FFFF99');
o.setFont('arial', 12);
o.addNode(11, 50, 'u', 'Facebook', 0, 'http://facebook.com');
o.addNode(13, 90, 'u', 'Youtube', 0, 'http://youtube.com');
o.addNode(14, 13, 'l', 'Youtube Music', 0, 'http://youtube.com/music');
o.addNode(15, 13, 'l', 'Youtube Entertainment', 0, 'http://youtube.com/entertainment');

o.setColor('#CC4950', '#FF7C80');
o.addNode(21, 0, 'l', 'Google', 0, 'http://google.com');
o.addNode(22, 0, 'l', 'Bing', 0, 'http://bing.com');

o.addNode('r2', '', '', 'Top of this Page', 0, '#');
o.addNode('', 'r2', 'u', 'Back to the introduction', 0, '/orgchart');

o.drawChart('c_links');

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

مطالب
نحوه استفاده از TransactionFlow در WCF
شش مرحله برای ایجاد WCFTransactions  در WCF 
 مقدمه و هدف:

هدف از مطلب  فوق اجرا نمودن عملیات Insert، Update و غیرو... بوسیله چندین Connection  در یک Transaction  در زمان اجرای سرویسهای WCF  میباشد. برای پیاده سازی و شرح Transaction ، سه پروژه ایجاد می‌نماییم. دو پروژه WCF  سرویس و یک پروژهClient ، هر سه پروژه را در یک Solution  به نام WCFTransaction  اضافه می‌نماییم. در هر دو پروژه WCF  بطور جداگانه Connection  رویDatabase  ایجاد می‌نماییم. سپس سعی می‌کنیم بوسیله Transaction  عملیات Insert  هر دو Service  را کنترل نماییم. بطوریکه اگر یکی از Service ‌ها در زمان عملیات Insert  دچار مشکل شود. دیگری نیز Commit  نگردد. به عبارتی در قدیم نمی‌توانستیم بیش از یک Connection  در یک Transaction  ایجاد نماییم. اما بوسیله Transactionscope ، انجام عملیات Insert، Update و غیرو...  بوسیله چندین Connection   به یکDatabase  بطور همزمان در یک Transaction  فراهم شده است. برای نمایش دادن عملیات Rollback  نیز،به عمد خطایی ایجاد می‌کنیم،تا نحوه Rollback  شدن در Transaction  را مشاهده نماییم.

سعی شده است پیاده سازی و استفاده از  Transaction در شش مرحله انجام شود.

مرحله اول: ایجاد دو پروژه WCFService و یک پروژه Client جهت فراخوانی (Call) کردن سرویسها

در این مرحله همانطور که از قیل نیز توضیح داده شده است، دو پروژه WCF  به نامهای WCFService1  و WCFService2  ایجاد شده است و یک پروژه Client  به نام WCFTransactions  نیز ایجاد می‌کنیم.

مرحله دوم : افزودن   Attribute ی به نام   TransactionFlow به  Interface سرویسها.

در این مرحله در Interface  هریک از سرویس‌ها متد جدیدی به نام UpdateData  اضافه می‌نماییم. که عملیات Insert into  درون Database  را انجام می‌دهد. حال بالای متد UpdateData   از صفت TransactionFlow  استفاده می‌نماییم. تا قابلیت Transaction  برای متد فوق فعال گردد و متد فوق اجازه می‌یابد از Transaction  استفاده نماید.

<ServiceContract()> _
Public Interface IService1

    <OperationContract()> _
    Function GetData(ByVal value As Integer) As String

    <OperationContract()> _
    Function GetDataUsingDataContract(ByVal composite As CompositeType) As CompositeType

    <OperationContract()> _
    <TransactionFlow(TransactionFlowOption.Allowed)> _
     Sub UpdateData()

End Interface

مرحله سوم:

در این مرحله متد UpdateData  را پیاده سازی می‌نماییم. بطوریکه یک Insert Into  ساده در Database  انجام می‌دهیم.و بالای متد فوق نیز کد زیر را می‌افزاییم.

 <OperationBehavior(TransactionScopeRequired:=True)> 

کد متد UpdateData   

   <OperationBehavior(TransactionScopeRequired:=True)> _
    Public Sub UpdateData() Implements IService1.UpdateData
        Dim objConnection As SqlConnection = New SqlConnection(strConnection)
        objConnection.Open()
        Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(10,10)", objConnection)
        objCommand.ExecuteNonQuery()
        objConnection.Close()
End Sub

مرحله دوم و سوم را برای Service دوم نیز تکرار می‌نماییم.

مرحله چهارم:

در این مرحله  TransactionFlow  را در Web.Config  دو سرویس فعال می‌نماییم. تا قابلیت استفاده از  TransactionFlow   برای سرویسها نیز فعال گردد. نحوه فعال نمودن بصورت زیر میباشد:

برای  WCFService1خواهیم داشت:

<bindings>
                <wsHttpBinding>
                                <binding name="TransactionalBind" transactionFlow="true"/>
                </wsHttpBinding>
</bindings>
و در ادامه داریم:
<endpoint address="" binding="wsHttpBinding" 
bindingConfiguration="TransactionalBind" 
contract="WcfService1.IService1">

برای  WCFService2نیز خواهیم داشت:

<bindings>
                <wsHttpBinding>
                                <binding name="TransactionalBind" transactionFlow="true"/>
                </wsHttpBinding>
</bindings>

و در ادامه داریم:

<endpoint address="" binding="wsHttpBinding" 
bindingConfiguration="TransactionalBind" 
contract="WcfService2.IService1">

مرحله پنجم:

در این مرحله دو سرویس فوق را به پروژه  WCFTransactions  اضافه نموده و قطعه کد زیر را درون فرم Load  می‌نویسیم.

Private Sub frmmain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        Using ts As New TransactionScope(TransactionScopeOption.Required)
            Try
                Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client()
                obj.UpdateData()
                Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client()
                obj1.UpdateData()
                ts.Complete()

            Catch ex As Exception
                ts.Dispose()
            End Try

        End Using
End Sub


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

مرحله ششم:

حال برای RollBack   کردن کل عملیات و مشاهده آنها کافیست در یکی از متدهای UpdateData  یک  Throw Exception  ایجاد نماییم.

سعی می‌کنیم با کمی تغییر در متد UpdateData در WCFService2 ، خطایی ایجاد شود، تا نحوه RollBack را مشاهده نماییم.

Public Sub UpdateData() Implements IService1.UpdateData
        Throw New Exception()
        Dim objConnection As SqlConnection = New SqlConnection(strConnection)
        objConnection.Open()
        Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(101,101)", objConnection)
        objCommand.ExecuteNonQuery()
        objConnection.Close()
End Sub

فقط کد زیر به متد UpdateData اضافه شده است:

Throw New Exception()

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


Using ts As New TransactionScope(TransactionScopeOption.Required)
            Try
                Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client()
                obj.UpdateData()
                Throw New Exception("There was Error")
                Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client()
                obj1.UpdateData()
                ts.Complete()

            Catch ex As Exception
                ts.Dispose()
            End Try
 End Using 

وقتی برنامه را اجرا نمایید، مشاهده می‌کنید که هیچ رکوردی دورن دیتابیس درج نشده است.

بسبار مهم: برای اینکه بتوانید بصورت Distibuted  عملیات Transaction  را انجام دهید می‌بایست تنظیماتی را روی سرور که دیتایس و سرویسها و کامپیوتر کلاینت انجام دهید که بصورت زیر می‌باشد:

نحوه تنظیم:

1- سرویسDistribute Transaction Coordinator  را روی هر دو Server‌های WCFService ، Database و کامپیوتر کلاینت، Start می‌نماییم.    

البته در شرایطی که Service‌های WCF و برنامه Client و Database روی یک سیستم باشد، تنظیمات فوق فقط روی همان سیستم انجام می‌شود.

برای دسترسی به قسمت Service ‌های Windows  ابتدا Administrative Tools  و سپس Service   را باز نمایید و روی Start کلیک کنید.

2- در ادامه روی MY Computer کلیک راست نموده و تب MSDTC را انتخاب نمایید:

در ادامه روی Security Configuration  کلیک نمایید. تا فرم زیر نمایش داده شود.


مطمئن شوید که آیتمهای زیر انتخاب شده باشند:

· Network DTC Access

· Allow Remote Clients

· Allow Inbound

· Allow Outbound

· Enable Transaction Internet Protocol(TIP) Transactions 

سپس با OK کردن Service،سرویس بطور خودکار Restart می‌شود.
در ضمن اگر از SQL Server 2000 استفاده می‌نمایید. لازم است تنظیم زیر را انجام دهید.
روی SQL Server Service Manager کلیک نموده و کامبوی Service را Dropdown نمایید و Distribute Transaction Coordinator  را انتخاب کنید. اما برای ورژن‌های بالاتر از SQL Server 2000 نیاز به انتخاب Distribute Transaction Coordinator  نمی‌باشد.
امیدوارم مطلب فوق مفید واقع شود، چنانچه کم و کاستی مشاهده نمودید، اینجانب را از نظرات خود بهره مند سازید.
منبع:
مطالب
نقدی بر کتاب «مرجع کامل entity framework 4.1»

کتاب «مرجع کامل entity framework 4.1» نوشته‌ی آقای راد نزدیک به یک ماهی است که منتشر شده است. فرصتی پیدا شد تا این کتاب حدودا 260 صفحه‌ای را مطالعه کنم و در ادامه توضیحاتی را پیرامون آن مطالعه خواهید کرد.

بررسی کتاب

در عنوان کتاب ذکر شده «مرجع کامل»؛ ولی خوب، 260 نمی‌تونه مرجع کامل باشه. بنابراین کمی رعایت اعتدال در کارهای بعدی لازم به نظر می‌رسد. همچنین یک مورد را هم همیشه در نشر کتب تخصصی در نظر داشته باشید: «ذکر شماره نگارش محصول» مورد نظر در عنوان کتاب، خیلی سریع کار شما را از مد افتاده خواهد کرد. خیالتان راحت باشد تا یک سال دیگر همینطور این شماره‌ها افزایش پیدا می‌کنند. خریداری هم که آنچنان اطلاعاتی از کل کار نداشته باشد، بر اساس همین شماره و بدون مطالعه متن، از خرید کتاب امتناع خواهد کرد.

  • فصل اول این کتاب به معرفی تاریخچه‌ی EF و لزوم استفاده از آن می‌پردازد. همچنین خلاصه‌ای از قابلیت‌های آن‌را همانند روش‌های model first ، database first و code first بیان می‌کند.
  • تمرکز فصل دوم بر نحوه‌ی استفاده از روش‌های model first و database first است به همراه نحوه‌ی تولید اسکریپت بانک اطلاعاتی در حالت model first.
  • فصل سوم کتاب به مرور جزئیات طراح EF در ویژوال استودیو جهت کار بهتر با موجودیت‌ها اختصاص دارد.
  • در فصل چهارم با روش‌های کوئری نویسی در EF آشنا خواهید شد. همچنین بر روی مباحث اجرای به تعویق افتاده و مفهوم آن هم بحث شده که بسیار ارزشمند است.
  • فصل پنجم کتاب به مباحث ثبت، حذف و به روز رسانی اطلاعات توسط EF اختصاص دارد. همچنین یک سری مباحث همانند سطح اول caching در NHibernate که در EF هم وجود دارد، بررسی شده است که البته نام آن در اینجا Object state و entity state است.
  • در فصل ششم در مورد نحوه‌ی نگاشت رویه‌های ذخیره شده SQL Server به اشیاء دات نتی بحث شده همچنین نحوه‌ی اجرا و استفاده از آن‌ها
  • فصل هفتم کتاب به ارتباطات بین موجود‌یت‌ها یا همان مباحث one to many و امثال آن اختصاص دارد به همراه نحوه‌ی تنظیمات آن در طراح EF در VS.NET
  • در فصل هشتم، به قالب‌های T4 پرداخته شده. ابتدا معرفی، سپس آشنایی با Syntax و نهایتا نحوه‌ی دستکاری و سفارشی سازی قالب‌های پیش فرض T4 مرتبط با EF ارائه شده‌اند.
  • فصل نهم به بررسی کاملتر مبحث model first که در فصل دوم معرفی شده می‌پردازد. ایجاد موجودیت‌ها، نحوه‌ی تعریف ارتباطات و نهایتا ایجاد بانک اطلاعاتی از روی آن
  • فصل دهم آن به مباحث جدید EF در مورد Code first اختصاص دارد. این فصل واقعا ارزشمند است چون ... نتیجه‌ی تحقیق بوده نه ترجمه. تقریبا با تمام تاریخچه‌ی مرتبط با code first در EF، محل‌های دریافت فایل‌ها، ابزارهای کمکی، روش‌های کوئری گرفتن،نحوه‌ی ایجاد بانک اطلاعاتی از روی کد، تعیین اعتبار و غیره در طی یک فصل آشنا خواهید شد.
  • در فصل یازدهم آن مروری بر WCF Data services و پروتکل OData صورت گرفته است. نحوه‌ی ایجاد و سپس فراخوانی آن توسط یک کلاینت. در عنوان کتاب ذکر شده : «مرجع»، بنابراین به دنبال یک کتاب خودآموز قدم به قدم نباشید. این کتاب بیشتر به «معرفی» امکانات موجود در EF در طی 260 صفحه می‌پردازد که الزاما با توجه به تعداد صفحات کتاب، بعضی از موارد آن مانند این فصل آخر، از عمق لازم برخوردار نیستند ولی، حداقل سرنخ را به دست شما خواهند داد.


مزایا:
  •  به روز بودن مطالب آن
  •  آشنایی و تسلط مؤلف/مترجم به مطالبی که تهیه کرده. این مورد در فصل دهم آن مشهود است.
  •  زبان فارسی (بله! خیلی مهمه! هستند کسانی که چند گیگ، ببخشید چند صد گیگ (!)، eBook به زبان انگلیسی دارند ولی حتی یکی از آن‌ها را هم تمام نکرده‌اند)
  •  متن روان و سلیس
  •  کیفیت خوب کتاب، صفحه بندی و امثال آن


معایب:
  •  قیمت نزدیک به 8000 تومان برای کتاب 260 صفحه‌ای به نظر زیاد است. البته با بالا رفتن قیمت‌ها (برای مثال 4 برابر شدن قیمت یک عدد نان لواش از سال قبل تا به امسال!)، بالاخره ... خوب این مسایل را هم به همراه خواهد داشت.
  •  تصاویر موجود در کتاب عموما بیش از اندازه کوچک شده‌اند. این مورد خواندن تعدادی از آن‌ها را با مشکل مواجه کرده است.
  •  در مورد متد الحاقی معروف Include در EF من مطلبی را در این کتاب پیدا نکردم. این مورد به بحث عدم نیاز به join نویسی صریح در EF مرتبط می‌شود.
  •  در مورد نحوه‌ی استفاده از EF با سایر بانک‌های اطلاعاتی بحث نشده. کتاب فقط به SQL Server منحصر است.
  •  در یکی از فصل‌ها به الگوی Repository در حد نامبردن اشاره شده. این مورد برای خواننده‌ای که اطلاعاتی از موضوع ندارد، کافی نیست. می‌شد یک فصل را به آن اختصاص داد.


در کل خواندن کتاب «معرفی» EF 4.1 ، به کسانی که با Silverlight و WCF RIA Services سر و کار دارند (و کوئری‌های آن برایشان کمی گنگ است) و همچنین عموم علاقمندانی که می‌خواهند جایگزینی برای ADO.NET (در یک سطح بالاتر از آن البته) پیدا کنند توصیه می‌شود.



در حاشیه!

شاید بپرسید چرا این کتاب در 260 صفحه و چرا فقط در 1000 نسخه منتشر شده است. چرا اینقدر تعداد کتاب‌های تخصصی کم است. چرا بیشتر تمایل به چاپ کتاب‌های نصب ویندوز و امثال آن است تا مثلا کتاب EF 4.1 یا خدای نکرده NHibernate ! پاسخ هم در یک جمله خلاصه می‌شود: «نگرانی ناشر از بازگشت سرمایه»
این شما هستید که با پشتیبانی خود می‌توانید این امیدواری را به ناشرین کشور بدهید تا «جرات کنند» بیشتر به طرف کتاب‌های تخصصی بروند و این پشتیبانی با صرفا گفتن چقدر عالی، دست شما درد نکنه، خیلی خوب بود، باز هم از این کارها بکنید،‌ معنا پیدا نمی‌کند! باید لطف کنید و «خرید کنید». هیچ راه دیگری هم ندارد. الان چند عدد کتاب ASP.NET MVC 3.0 در کشور به زبان فارسی وجود دارد؟ چند عدد کتاب تخصصی SQL Server 2008 R2 را می‌توانید پیدا کنید؟ در مورد کتابخانه پردازش موازی دات نت 4 چطور؟ و ...
البته منهای نگرانی این بحث بازگشت سرمایه ، یک مورد دیگر هم سبب این نوع تاخیرها هست. یادم میاد کتاب الگوهای طراحی برنامه نویسی شیءگرا در سی شارپ رو که چند سال قبل به ناشر دادم نحوه‌ی پرداخت آن به این صورت بود: نزدیک به 10 درصد پشت جلد، در طی چند قسط، آن هم 6 ماه پس از انتشار عمومی کتاب! خوب همین شد که من دیگر به طرف این کار نرفتم. چون واقعا نوشتن، یک «کار» کامل است. باید وقت گذاشت (6 ماه حداقل یا بیشتر)، تحقیق کرد، ریاضت کشید و دست آخر 6 ماه پس از انتشار کتاب ... با توجه به اینکه کتاب رو که الان شما به دست ناشر می‌دید شاید یکسال دیگر منتشر شود (بسته به تعداد کاری که در دست دارد).
در هر حال، با تمام این تفاسیر، هستند کسانی که «امیدوارانه» نسبت به نوشتن کتاب‌های تخصصی مانند «مرجع کامل entity framework 4.1» اقدام می‌کنند و شما هم حداقل کاری که می‌توانید جهت حمایت از این نوع حرکات بکنید، «خرید است». در غیراینصورت مدام اینطرف اونطرف ننویسید که چرا کتاب WPF 4.0 یا WCF 4.0 به زبان فارسی نداریم. پشتیبانی نمی‌کنید؟! خوب ... نداریم! «همین!»
یک مورد دیگر هم هست البته. عده‌ای هستند که مثلا کلاس‌های میلیونی، جهت آموزش این مباحث برگزار می‌کنند. خوب این‌ها هم مسلما خوشحال نخواهند شد که مثلا کتاب WCF 4.0 و مباحث SOA مرتبط با آن به زبان فارسی منتشر شود یا حتی در این زمینه پیش قدم شوند. این هم هست!


مسیرراه‌ها
Blazor 5x

مبانی Blazor 

 احراز هویت و اعتبارسنجی کاربران Blazor Server

تهیه API مخصوص Blazor WASM
Blazor WASM 

احراز هویت و اعتبارسنجی کاربران Blazor WASM

توزیع برنامه 

مدیریت استثناءها

بررسی تغییرات Blazor 8x  

مطالب تکمیلی
مطالب
بررسی تغییرات Blazor 8x - قسمت هشتم - مدیریت انتقال اطلاعات Pre-Rendering سمت سرور، به جزایر تعاملی
این Prerendering است که امکان رندر یک کامپوننت تعاملی را در سمت سرور میسر می‌کند تا کاربر بتواند پیش از فعال شدن قابلیت‌های پیشرفته‌ی یک کامپوننت، یک حداقل خروجی را از آن مشاهده کند و همچنین وجود آن برای موتورهای جستجو و بهبود SEO بسیار مفید است. اما ... در این بین مشکلی رخ می‌دهد که نمونه‌ی آن‌را در قسمت قبل مشاهده کردیم: آغاز آن دوبار صورت می‌گیرد؛ یکبار در سمت سرور برای تهیه‌ی یک خروجی SSR و یکبار هم پس از فعال شدن قابلیت‌های تعاملی آن در سمت کلاینت. این آغاز دوباره، برای هر دو حالت کامپوننت‌های تعاملی Blazor Server و Blazor WASM برقرار است. راه‌حل‌هایی از نحوه‌ی مواجه شدن با یک چنین مشکلی را در قسمت قبل بررسی کردیم. راه‌حل دیگری که در این بین ارائه شده و توسط خود مایکروسافت هم در مثال‌های آن مورد استفاده قرار می‌گیرد، استفاده از سرویس PersistentComponentState است که جزئیات آن‌را در این قسمت بررسی خواهیم کرد.


بررسی نحوه‌ی عملکرد سرویس PersistentComponentState

سرویس PersistentComponentState، در دات‌نت 6، به Blazor اضافه شد و امکان جدیدی نیست. قسمتی از این مباحث جدید SSR که به‌نظر مختص به Blazor 8x هستند، پیشتر هم وجود داشتند؛ تحت عنوان pre-rendering. برای مثال فقط کافی بودن تا در برنامه‌های Blazor Server قبلی، فایل Host.cshtml_ را به صورت زیر ویرایش کرد تا pre-rendering فعال شود:
<component type="typeof(App)" render-mode="ServerPrerendered" />
مشکلی که در این حالت بروز می‌کرد این بود که متد OnInitializedAsync یک کامپوننت، دوبار فراخوانی می‌شد؛ یکبار در زمان pre-rendering در سمت سرور، تا HTML استاتیکی برای ارائه‌ی به مرورگر کاربر تولید شود و بار دیگر در زمان فعال شدن اتصال SignalR کامپوننت و ارائه‌ی نهایی تعاملی آن. به همین جهت، کار سنگین آغازین یک کامپوننت، دوبار انجام می‌شد که تجربه‌ی کاربری ناخوشایندی را هم به همراه داشت. برای رفع این مشکل، tag helper ویژه‌ای را به نام persist-component-state تهیه کردند که به صورت زیر به فایل host.cshtml_ اضافه می‌شد:
<body>
    <component type="typeof(App)" render-mode="WebAssemblyPrerendered" /> 
    <persist-component-state />
</body>
این tag-helper فوق است که کار درج رمزنگاری شده‌ی اطلاعات کش شده‌ی pre-rendering سمت سرور را در انتهای فایل HTML صفحه انجام می‌دهد و برای نمونه، نحوه‌ی استفاده‌ی از آن به صورت زیر است:
@page "/"

@implements IDisposable
@inject PersistentComponentState applicationState
@inject IUserRepository userRepository

@foreach(var user in users)
{
    <ShowUserInformation user="@item"></ShowUserInformation>
}

@code {
    private const string cachingKey = "something_unique";
    private List<User> users = new();
    private PersistingComponentStateSubscription persistingSubscription;
    
    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = applicationState.RegisterOnPersisting(PersistData);

        if (applicationState.TryTakeFromJson<List<User>>(cachingKey, out var restored))
        {
            users = restored;
        }
        else 
        {
            users = await userRepository.GetAllUsers();
        }
    }

    public void Dispose()
    {
        persistingSubscription.Dispose();
    }

    private Task PersistData()
    {
        applicationState.PersistAsJson(cachingKey, users);
        return Task.CompletedTask;
    }

}
توضیحات:
- ابتدا تزریق سرویس PersistentComponentState را مشاهده می‌کنید. این همان کامپوننتی است که کار کش کردن اطلاعات را مدیریت می‌کند.
- سپس فراخوانی متد RegisterOnPersisting انجام شده‌است. متدی که توسط آن ثبت می‌شود، در حین عملیات pre-rendering فراخوانی می‌شود تا اطلاعاتی را کش کند. نحوه‌ی این کش شدن را در ادامه‌ی مطلب بررسی می‌کنیم.
- سپس فراخوانی متد TryTakeFromJson رخ‌داده‌است. اگر این متد اطلاعاتی را برگرداند، یعنی pre-rendering سمت سرور پیشتر انجام شده و اطلاعاتی کش شده، برای دریافت در سمت کلاینت، وجود داشته و نیازی به مراجعه‌ی مجدد و دوباره، به بانک اطلاعاتی نیست.
- دراینجا یک قسمت Dispose را هم مشاهده می‌کنید تا اگر کاربر به صفحه‌ی دیگری مراجعه کرد، متد ثبت شده‌ی در اینجا، از لیست مواردی که باید در حین pre-rendering سریالایز شوند، حذف گردد.

اگر از این روش استفاده نشود، به علت دوبار فراخوانی شدن متد OnInitializedAsync یک کامپوننت به همراه pre-rendering، اطلاعاتی که در حین pre-rendering کامپوننت از بانک اطلاعاتی، برای تولید محتوای استاتیک صفحه در سمت سرور دریافت شده، با فعالسازی آتی تعاملی سمت کلاینت آن کامپوننت، از دست خواهد رفت و در این حالت کلاینت باید مجددا این اطلاعات را از بانک اطلاعاتی درخواست کند. بنابراین هدف از persist-component-state، حفظ داده‌ها در بین دو رندر است؛ رندر اولی که در سمت سرور انجام می‌شود و رندر دومی که متعاقبا در سمت کلاینت رخ می‌دهد.

یک نکته: به یک چنین قابلیتی در فریم‌ورک‌های دیگر «hydration» هم گفته می‌شود که در اصل یک شیء دیکشنری است که مقدار شیء حالت را در سمت سرور دریافت کرده و آن‌را در زمان فعال شدن سمت کلاینت کامپوننت، برای استفاده‌ی مجدد مهیا می‌کند.


سؤال: اطلاعات سرویس PersistentComponentState کجا ذخیره می‌شوند؟

همانطور که در مثال فوق هم مشاهده کردید، سرویس PersistentComponentState، این state را به صورت JSON ذخیره می‌کند. اما ... این اطلاعات دقیقا کجا ذخیره می‌شوند؟
برای مشاهده‌ی آن‌ها، فقط کافی است به source صفحه مراجعه کنید تا با دو مقدار مخفی رمزنگاری شده‌ی زیر در انتهای HTML صفحه مواجه شوید!
<!--Blazor-Server-Component-State:CfDJXyz->
<!--Blazor-WebAssembly-Component-State:eyJVc2Xyz->
فرمت این اطلاعات، JsonSerializer.SerializeToUtf8Bytes رمزنگاری شده‌ی توسط IDataProtectionProvider است. این هم یک روش نگهداری اطلاعات، بجای استفاده از کش مرورگر است؛ بدون نیاز به استفاده از جاوااسکریپت و کار با local storage و امثال آن.

مایکروسافت از این نوع کارها پیشتر در ASP.NET Web forms توسط ViewStateها انجام داده‌است! البته ViewStateها جهت نگهداری اطلاعات وضعیت کلاینت، به سمت سرور ارسال می‌شوند؛ اما این Component-Stateهای مخفی، فقط یکبار توسط قسمت کلاینت خوانده می‌شوند و هدف آن‌ها ارسال اطلاعاتی به سمت سرور نیست.

یک نکته: همانطور که عنوان شد، در نگارش‌های قبلی Blazor، از تگ‌هلپری به نام persist-component-state برای درج این اطلاعات در انتهای صفحه استفاده می‌کردند. استفاده از این تگ‌هلپر در Blazor 8x منسوخ شده و به صورت خودکار داده‌های تمام سرویس‌های PersistentComponentState فعالی که توسط PersistAsJson اطلاعاتی را ذخیره می‌کنند، جمع آوری و به صورت یکجا در انتهای صفحه به صورت رمزنگاری شده، درج می‌کنند.


روش خاموش کردن Pre-rendering

برای خاموش کردن pre-rendering نیاز است پارامتر هم‌نامی را به نحو زیر با false مقدار دهی کرد:
@rendermode renderMode

@code 
{
    private static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false);
}


بازنویسی مثال قسمت قبل با استفاده از سرویس PersistentComponentState

در قسمت قبل هرچند روش مواجه شدن با مشکل دوبار فراخوانی شدن متد آغازین یک کامپوننت را در سمت سرور و کلاینت بررسی کردیم، اما ... در نهایت دوبار مراجعه به بانک اطلاعاتی انجام می‌شود؛ یکبار در زمان pre-rendering و با مراجعه‌ی مستقیم به یک سرویس سمت سرور و یکبار دیگر هم در زمان فراخوانی httpClient.GetFromJsonAsync در سمت کلاینت برای دریافت اطلاعات مورد نیاز از یک Web API Endpoint. اگر بخواهیم اطلاعات لیست محصولات دریافتی سمت سرور را به سمت کلاینت منتقل کنیم و مجددا آن‌را از بانک اطلاعاتی دریافت نکنیم، راه‌حل سوم آن، استفاده از سرویس PersistentComponentState است.

در کدهای زیر، بازنویسی کامپوننت محصولات مشابه را مشاهده می‌کنید که کمی پیچیده‌تر شده‌است. اینبار لیست محصولات مشابه، در همان بار اول رندر کامپوننت نمایش داده می‌شوند و نه پس از کلیک بر روی دکمه‌ای. به همین جهت باید کار مدیریت دوبار فراخوانی متد رویدادگردان OnInitializedAsync را به درستی انجام داد. متد OnInitializedAsync یکبار در حین پیش‌رندر سمت سرور اجرا می‌شود و بار دیگر زمانیکه وب‌اسمبلی جاری در مرورگر به صورت کامل دریافت شده و فعال می‌شود؛ یعنی برای بار دوم، کدهای اجرایی آن سمت کلاینت هستند.
در این مثال با استفاده از سرویس PersistentComponentState، اطلاعات دریافت شده‌ی در حین pre-rendering ابتدایی رخ‌داده‌ی در سمت سرور، در متد OnPersistingAsync، کش شده و در حین فعال شدن وب‌اسمبلی مرتبط در سمت کلاینت، با استفاده از متد PersistentState.TryTakeFromJson، از کش خوانده می‌شوند و دیگر دوبار رفت و برگشت به بانک اطلاعاتی را شاهد نخواهیم بود.

@implements IDisposable

@inject IProductStore ProductStore
@inject PersistentComponentState PersistentState

<h3>Related products</h3>

@if (_relatedProducts == null)
{
    <p>Loading...</p>
}
else
{
    <div class="mt-3">
        @foreach (var item in _relatedProducts)
        {
            <a href="/ProductDetails/@item.Id">
                <div class="col-sm">
                    <h5 class="mt-0">@item.Title (@item.Price)</h5>
                </div>
            </a>
        }
    </div>
}

@code{

    private const string StateCachingKey = "products";
    private IList<Product>? _relatedProducts;
    private PersistingComponentStateSubscription _persistingSubscription;

    [Parameter]
    public int ProductId { get; set; }

    protected override async Task OnInitializedAsync()
    {
        _persistingSubscription = PersistentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);

        if (PersistentState.TryTakeFromJson<IList<Product>>(StateCachingKey, out var restored))
        {
            _relatedProducts = restored;
        }
        else
        {
            await Task.Delay(500); // Simulates asynchronous loading
            _relatedProducts = await ProductStore.GetRelatedProducts(ProductId);
        }
    }

    private Task OnPersistingAsync()
    {
        PersistentState.PersistAsJson(StateCachingKey, _relatedProducts);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _persistingSubscription.Dispose();
    }

}
در این پیاده سازی هنوز هم از سرویس IProductStore استفاده می‌شود که دو نگارش سمت سرور و سمت کلاینت آن‌را در قسمت قبل تهیه کردیم. برای مثال باتوجه به اینکه کدهای فوق در حین pre-rendering در سمت سرور اجرا می‌شوند، به صورت خودکار از نگارش سمت سرور IProductStore استفاده خواهد شد.

نکته‌ی مهم: فعلا کدهای فوق فقط برای حالت بارگذاری اولیه‌ی کامپوننت درست کار می‌کنند. یعنی اگر نگارش وب‌اسمبلی کامپوننت پس از وقوع پیش‌رندر سمت سرور، در مرورگر دریافت و بارگذاری کامل شود؛ اما برای دفعات بعدی مراجعه‌ی به این صفحه با استفاده از enhanced navigation و راهبری بهبود یافته که در قسمت ششم این سری بررسی کردیم ... دیگر کار نمی‌کنند و در این حالت کش شدنی رخ نمی‌دهد که در نتیجه، شاهد دوبار رفت و برگشت به بانک اطلاعاتی خواهیم بود و اساسا استفاده‌ی از PersistentComponentState را زیر سؤال می‌برد؛ چون در حین راهبری بهبود یافته، از آن استفاده نمی‌شود (قسمت PersistentState.TryTakeFromJson آن، هیچگاه اطلاعاتی را از کش نمی‌خواند). اطلاعات بیشتر 
مطالب
آزمایش ساده‌تر Web APIs توسط strest
در سری کار با Postman، یک روش بسیار متداول آزمایش Web APIs را بررسی کردیم. اما ... برای کار آن با مدام نیاز است از این برگه به آن برگه مراجعه کرد و ارتباط دادن درخواست‌های متوالی در آن مشکل است. به همین منظور تابحال راه‌حل‌های زیادی برای جایگزین کردن postman ارائه شده‌اند که یکی از آن‌ها strest است. این ابزار خط فرمان:
- بسیار سبک ورزن است و تنها نیاز به نصب بسته‌ی npm آن‌را دارد.
- با فایل‌های متنی معمولی کار می‌کند که ویرایش و copy/paste در آن‌ها بسیار ساده‌است.
- قرار دادن فایل‌های نهایی متنی آن در ورژن کنترل بسیار ساده‌است.
- امکان نوشتن درخواست‌های به هم وابسته و آزمودن نتایج حاصل را دارا است.
- چون یک ابزار خط فرمان است، امکان استفاده‌ی از آن به سادگی در فرآینده‌های توسعه‌ی مداوم وجود دارد.
- ابزارهای npm، چندسکویی هستند.


نصب strest

در ادامه قصد داریم مطلب «آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT» را با استفاده از strest بازنویسی کنیم. به همین جهت در ابتدا نیاز است بسته‌ی npm آن‌را به صورت سراسری نصب کنیم:
npm i -g @strest/cli
پس از آن فایل جدید JWT.strest.yml را در پوشه‌ای ایجاد کرده و آن‌را تکمیل می‌کنیم. برای اجرای فرامین موجود در آن تنها کافی است دستور strest JWT.strest.yml را درخط فرمان صادر کنیم.


مرحله 1: خاموش کردن بررسی مجوز SSL برنامه
مرحله 2: ایجاد درخواست login و دریافت توکن‌ها

مجوز SSL آزمایشی برنامه‌ی ASP.NET Core ما، از نوع خود امضاء شده‌است. به همین جهت اگر سعی در اجرای strest را با درخواست‌های ارسالی به آن داشته باشیم، باشکست مواجه خواهند شد. بنابراین در ابتدا، خاصیت allowInsecure را به true تنظیم می‌کنیم:
version: 2

variables:
  baseUrl: https://localhost:5001/api
  logResponse: false

allowInsecure: true
- این تنظیمات با فرمت yaml نوشته می‌شوند. به همین جهت در اینجا تعداد spaceها مهم است.
- همچنین در ابتدای این تنظیمات، روش تعریف متغیرها را نیز مشاهده می‌کنید که برای مثال توسط آن‌ها baseUrl تعریف شده‌است.
درست در سطر پس از این تنظیمات، دستور اجرا و اعتبارسنجی درخواست Login را می‌نویسیم:
requests:
  loginRequest:
    request:
      url: <$ baseUrl $>/account/login
      method: POST
      postData:
        mimeType: application/json
        text:
          username: "Vahid"
          password: "1234"
    log: <$ logResponse $>
    validate:
      - jsonpath: content.access_token
        type: [string]
      - jsonpath: content.refresh_token
        type: [string]
توضیحات:
- درخواست‌ها با requests شروع می‌شوند. سپس ذیل آن می‌توان نام چندین درخواست یا request را ذکر کرد که برای مثال نام درخواست تعریف شده‌ی در اینجا loginRequest است. این نام مهم است؛ از این جهت که با اشاره‌ی به آن می‌توان به فیلدهای خروجی response حاصل، در درخواست‌های بعدی، دسترسی یافت.
- سپس، آدرس درخواست مشخص شده‌است. در اینجا روش کار با متغیرها را نیز مشاهده می‌کنید.
- نوع درخواست POST است.
- در ادامه جزئیات اطلاعات ارسالی به سمت سرور باید مشخص شوند. برای مثال در اینجا با فرمت application/json قرار است یک شیء تشکیل شده‌ی از username و password ارسال شوند.
- در سطر بعدی، خاصیت log با متغیر logResponse مقدار دهی شده‌است. اگر به true تنظیم شود، اصل خروجی response را توسط برنامه‌ی خط فرمان strest می‌توان مشاهده کرد. اگر اینکار خروجی را شلوغ کرد، می‌توان آن‌را به false تنظیم کرد و این خروجی را در فایل strest_history.json نهایی که حاصل از اجرای آزمایش‌های تعریف شده‌است، در کنار فایل JWT.strest.yml خود یافت و مشاهده کرد.
- سپس به قسمت آزمودن نتیجه‌ی درخواست می‌رسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشته‌ای access_token و refresh_token باشد.


 مرحله‌ی 3: ذخیره سازی توکن‌های دریافتی در متغیرهای سراسری
 مرحله‌ی 3: ذخیره سازی مراحل انجام شده
در حین کار با strest نیازی به ذخیره سازی نتیجه‌ی حاصل از response، در متغیرهای خاصی نیست. برای مثال اگر بخواهیم به نتیجه‌ی حاصل از عملیات لاگین فوق در درخواست‌های بعدی دسترسی پیدا کنیم، می‌توان نوشت <$ loginRequest.content.access_token $>
در اینجا درج متغیرها توسط <$ $> صورت می‌گیرد. سپس loginRequest به نام درخواست مرتبط اشاره می‌کند. خاصیت content.access_token نیز مقدار خاصیت access_token شیء response را بر می‌گرداند.

همچنین ذخیره سازی مراحل انجام شده نیز نکته‌ی خاصی را به همراه ندارد. یک تک فایل متنی JWT.strest.yml وجود دارد که آزمایش‌های ما در آن درج می‌شوند.


مرحله‌ی 4: دسترسی به منابع محافظت شده‌ی سمت سرور

در ادامه روش تعریف دو درخواست جدید دیگر را در فایل JWT.strest.yml مشاهده می‌کنید که از نوع Get هستند و به اکشن متدهای محافظت شده ارسال می‌شوند:
  myProtectedApiRequest:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ loginRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.title
        expect: "Hello from My Protected Controller! [Authorize]"

  mProtectedAdminApiRequest:
    request:
      url: <$ baseUrl $>/MyProtectedAdminApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ loginRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.title
        expect: "Hello from My Protected Admin Api Controller! [Authorize(Policy = CustomRoles.Admin)]"
دو نکته‌ی جدید در اینجا قابل مشاهده‌است:
- چون نیاز است به همراه درخواست خود، هدر اعتبارسنجی مبتنی بر JWT را که به صورت Bearer value است نیز به سمت سرور ارسال کنیم، خاصیت headers را توسط یک name/value مشخص کرده‌ایم. همانطور که عنوان شد در فایل‌های yaml، فاصله‌ها و تو رفتگی‌ها مهم هستند و حتما باید رعایت شوند.
- سپس دومین آزمون نوشته شده را نیز مشاهده می‌کنید. در قسمت validate، مشخص کرده‌ایم که خاصیت title دریافتی از response باید مساوی مقدار خاصی باشد.

دقیقا همین نکات برای درخواست دوم به MyProtectedAdminApi تکرار شده‌اند.


مرحله‌ی 5: ارسال Refresh token و دریافت یک سری توکن جدید

اکشن متد account/RefreshToken در سمت سرور، نیاز دارد تا یک شیء جی‌سون با خاصیت refreshToken را دریافت کند. مقدار این خاصیت از طریق response متناظر با درخواست نام‌دار loginRequest استخراج می‌شود که در قسمت postData مشخص شده‌است:
  refreshTokenRequest:
    request:
      url: <$ baseUrl $>/account/RefreshToken
      method: POST
      postData:
        mimeType: application/json
        text:
          refreshToken: <$ loginRequest.content.refresh_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.access_token
        type: [string]
      - jsonpath: content.refresh_token
        type: [string]
در آخر، به قسمت آزمودن نتیجه‌ی درخواست می‌رسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشته‌ای access_token و refresh_token باشد که بیانگر صدور توکن‌های جدیدی هستند.


مرحله‌ی 6: آزمایش توکن جدید دریافتی از سرور

در قسمت قبل، توکن‌های جدیدی صادر شدند که اکنون برای کار با آن‌ها می‌توان از متغیر refreshTokenRequest.content.access_toke استفاده کرد:
  myProtectedApiRequestWithNewToken:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ refreshTokenRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content.title
        expect: "Hello from My Protected Controller! [Authorize]"
در اینجا با استفاده از توکن جدید درخواست نام‌دار refreshTokenRequest، آزمون واحد نوشته شده با موفقیت به پایان می‌رسد (یا باید برسد که اجرای نهایی آزمایش‌ها، آن‌را مشخص می‌کند).


مرحله‌ی 7: آزمایش منقضی شدن توکنی که در ابتدای کار پس از لاگین دریافت کردیم

اکنون که refresh token صورت گرفته‌است، دیگر نباید بتوانیم از توکن دریافتی پس از لاگین استفاده کنیم و برنامه باید آن‌را برگشت بزند:
  myProtectedApiRequestWithOldToken:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ loginRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: status
        expect: 401
به همین جهت، درخواستی ارسال شده که به نتیجه‌ی درخواست نام‌دار loginRequest اشاره می‌کند. در این حالت برای آزمایش عملیات، اینبار status بازگشتی از سرور که باید 401 باشد، بررسی شده‌است.


مرحله‌ی 8: آزمایش خروج از سیستم

در اینجا نیاز است به آدرس account/logout، یک کوئری استرینگ را با کلید refreshToken و مقدار ریفرش‌توکن دریافتی از درخواست نام‌دار refreshTokenRequest، به سمت سرور ارسال کنیم:
  logoutRequest:
    request:
      url: <$ baseUrl $>/account/logout
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ refreshTokenRequest.content.access_token $>
      queryString:
        - name: refreshToken
          value: <$ refreshTokenRequest.content.refresh_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: content
        expect: true
خروجی آزمایش شده‌ی در اینجا، دریافت مقدار true از سمت سرور است.


مرحله‌ی 9: بررسی عدم امکان دسترسی به منابع محافظت شده‌ی سمت سرور، پس از logout

در مرحله‌ی قبل، از سیستم خارج شدیم. اکنون می‌خواهیم بررسی کنیم که آیا توکن دریافتی پیشین هنوز معتبر است یا خیر؟ آیا می‌توان هنوز هم به منابع محافظت شده دسترسی یافت یا خیر:
  myProtectedApiRequestWithNewTokenAfterLogout:
    request:
      url: <$ baseUrl $>/MyProtectedApi
      method: GET
      headers:
        - name: Authorization
          value: Bearer <$ refreshTokenRequest.content.access_token $>
    log: <$ logResponse $>
    validate:
      - jsonpath: status
        expect: 401
به همین جهت هدر Authorization را با اکسس‌توکنی که در مرحله‌ی ریفرش‌توکن دریافت کردیم (پیش از logout)، مقدار دهی می‌کنیم و سپس درخواستی را به یک منبع محافظت شده ارسال می‌کنیم. نتیجه‌ی حاصل باید status code ای مساوی 401 داشته باشد که به معنای برگشت خوردن آن است


مرحله‌ی 10: اجرای تمام آزمون‌های واحد نوشته شده

همانطور که در ابتدای بحث نیز عنوان شد فقط کافی است دستور strest JWT.strest.yml را در خط فرمان اجرا کنیم تا آزمون‌های ما به ترتیب اجرا شوند:


فایل نهایی این آزمایش را در اینجا می‌توانید مشاهده می‌کنید.
مطالب
React 16x - قسمت 26 - احراز هویت و اعتبارسنجی کاربران - بخش 1 - ثبت نام و ورود به سیستم
می‌خواهیم به برنامه‌ی لیست فیلم‌هایی که تا این قسمت تکمیل کردیم، امکانات جدیدی را مانند ورود به سیستم، خروج از آن، کار با JWT، فراخوانی منابع محافظت شده‌ی سمت سرور، نمایش و یا مخفی کردن المان‌های صفحه بر اساس سطوح دسترسی کاربر و همچنین محافظت از مسیرهای مختلف تعریف شده‌ی در برنامه، اضافه کنیم.
برای قسمت backend، از همان برنامه‌ی تکمیل شده‌ی قسمت قبل استفاده می‌کنیم که به آن تولید مقدماتی JWTها نیز اضافه شده‌است. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت React است. بنابراین اینکه چگونه این توکن را تولید می‌کنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روش‌های پیاده سازی سمت سرور سازگار است (و مختص به فناوری خاصی نیست). پیشنیاز درک کدهای سمت سرور قسمت JWT آن، مطالب زیر هستند:
  1. «معرفی JSON Web Token»
  2. «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» 
  3. «پیاده سازی JSON Web Token با ASP.NET Web API 2.x»
  4. « آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT»  


ثبت یک کاربر جدید

فرم ثبت نام کاربران را در قسمت 21 این سری، در فایل src\components\registerForm.jsx، ایجاد و تکمیل کردیم. البته این فرم هنوز به backend server متصل نیست. برای کار با آن هم نیاز است شیءای را با ساختار زیر که ذکر سه خاصیت اول آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Users به صورت یک HTTP Post ارسال کنیم:
{
  "name": "string",
  "email": "string",
  "password": "string",
  "isAdmin": true,
  "id": 0
}
در سمت سرور هم در Services\UsersDataSource.cs که در انتهای بحث می‌توانید پروژه‌ی کامل آن‌را دریافت کنید، منحصربفرد بودن ایمیل وارد شده بررسی می‌شود و اگر یک رکورد دو بار ثبت شود، یک BadRequest را به همراه پیام خطایی، بازگشت می‌دهد.

اکنون نوبت به اتصال کامپوننت registerForm.jsx، به سرویس backend است. تا اینجا دو سرویس src\services\genreService.js و src\services\movieService.js را در قسمت قبل، به برنامه جهت کار کردن با endpoint‌های backend server، اضافه کردیم. شبیه به همین روش را برای کار با سرویس سمت سرور api/Users نیز در پیش می‌گیریم. بنابراین فایل جدید src\services\userService.js را با محتوای زیر، به برنامه‌ی frontend اضافه می‌کنیم:
import http from "./httpService";
import { apiUrl } from "../config.json";

const apiEndpoint = apiUrl + "/users";

export function register(user) {
  return http.post(apiEndpoint, {
    email: user.username,
    password: user.password,
    name: user.name
  });
}
توسط متد register این سرویس می‌توانیم شیء user را با سه خاصیت مشخص شده، از طریق HTTP Post، به آدرس api/Users ارسال کنیم. خروجی این متد نیز یک Promise است. در این سرویس، تمام متدهایی که قرار است با این endpoint سمت سرور کار کنند، مانند ثبت، حذف، دریافت اطلاعات و غیره، تعریف خواهند شد.
اطلاعات شیء user ای که در اینجا دریافت می‌شود، از خاصیت data کامپوننت RegisterForm تامین می‌گردد:
class RegisterForm extends Form {
  state = {
    data: { username: "", password: "", name: "" },
    errors: {}
  };
البته اگر دقت کرده باشید، در شیء منتسب به خاصیت data، خاصیتی به نام username تعریف شده‌است، اما در سمت سرور، نیاز است خاصیتی با نام Name را دریافت کنیم. یک چنین نگاشتی در داخل متد register سرویس کاربر، قابل مشاهده‌‌است. در غیراینصورت می‌شد در متد http.post، کل شیء user را به عنوان پارامتر دوم، درنظر گرفت و ارسال کرد.

پس از تعریف userService.js، به registerForm.jsx بازگشته و ابتدا امکانات آن‌را import می‌کنیم:
import * as userService from "../services/userService";
می‌شد این سطر را به صورت زیر نیز نوشت، تا تنها یک متد از ماژول userService را دریافت کنیم:
import { register } userService from "../services/userService";
اما روش as userService * به معنای import تمام متدهای این ماژول است. به این ترتیب نام ذکر شده‌ی پس از as، به عنوان شیءای که می‌توان توسط آن به این متدها دسترسی یافت، قابل استفاده می‌شود؛ مانند فراخوانی متد userService.register. اکنون می‌توان متد doSubmit این فرم را به سرور متصل کرد:
  doSubmit = async () => {
    try {
      await userService.register(this.state.data);
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors }; // clone an object
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };


مدیریت و نمایش خطاهای دریافتی از سمت سرور

در این حالت برای ارسال اطلاعات یک کاربر، در بار اول، یک چنین خروجی را از سمت سرور می‌توان شاهد بود که id جدیدی را به این رکورد نسبت داده‌است:


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


که از نوع 400 یا همان BadRequest است:


بنابراین نیاز است بدنه‌ی response را در یک چنین مواردی که خطایی از سمت سرور صادر می‌شود، دریافت کرده و با به روز رسانی خاصیت errors در state فرم (همان قسمت بدنه‌ی catch کدهای فوق)، سبب درج و نمایش خودکار این خطا شویم:


پیشتر در قسمت بررسی «کار با فرم‌ها» آموختیم که برای مدیریت خطاهای پیش بینی شده‌ی دریافتی از سمت سرور، نیاز است قطعه کدهای مرتبط با سرویس http را در بدنه‌ی try/catch‌ها محصور کنیم. برای مثال در اینجا اگر ایمیل شخصی تکراری وارد شود، سرویس یک return BadRequest("Can't create the requested record.") را بازگشت می‌دهد که در اینجا status code معادل BadRequest، همان 400 است. بنابراین انتظار داریم که خطای 400 را از سمت سرور، تحت شرایط خاصی دریافت کنیم. به همین دلیل است که در اینجا تنها مدیریت status code=400 را در بدنه‌ی catch نوشته شده ملاحظه می‌کنید.
سپس برای نمایش آن، نیاز است خاصیت متناظری را که این خطا به آن مرتبط می‌شود، با پیام دریافت شده‌ی از سمت سرور، مقدار دهی کنیم که در اینجا می‌دانیم مرتبط با username است. به همین جهت سطر errors.username = ex.response.data، کار انتساب بدنه‌ی response را به خاصیت جدید errors.username انجام می‌دهد. در این حالت اگر به کمک متد setState، کار به روز رسانی خاصیت errors موجود در state را انجام دهیم، رندر مجدد فرم، در صف انجام قرار گرفته و در رندر بعدی آن، پیام موجود در errors.username، نمایش داده می‌شود.


پیاده سازی ورود به سیستم

فرم ورود به سیستم را در قسمت 18 این سری، در فایل src\components\loginForm.jsx، ایجاد و تکمیل کردیم که این فرم نیز هنوز به backend server متصل نیست. برای کار با آن نیاز است شیءای را با ساختار زیر که ذکر هر دو خاصیت آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Auth/Login به صورت یک HTTP Post ارسال کنیم:
{
  "email": "string",
  "password": "string"
}
با ارسال این اطلاعات به سمت سرور، درخواست Login انجام می‌شود. سرور نیز در صورت تعیین اعتبار موفقیت آمیز کاربر، به صورت زیر، یک JSON Web token را بازگشت می‌دهد:
var jwt = _tokenFactoryService.CreateAccessToken(user);
return Ok(new { access_token = jwt });
یعنی بدنه‌ی response رسیده‌ی از سمت سرور، دارای یک شیء JSON خواهد بود که خاصیت access_token آن، حاوی JSON Web token متعلق به کاربر جاری لاگین شده‌است. در آینده اگر این کاربر نیاز به دسترسی به یک api endpoint محافظت شده‌ای را در سمت سرور داشته باشد، باید این token را نیز به همراه درخواست خود ارسال کند تا پس از تعیین اعتبار آن توسط سرور، مجوز دسترسی به منبع درخواستی برای او صادر شود.

در ادامه برای تعامل با منبع api/Auth/Login سمت سرور، ابتدا یک سرویس مختص آن‌را در فایل جدید src\services\authService.js، با محتوای زیر ایجاد می‌کنیم:
import { apiUrl } from "../config.json";
import http from "./httpService";

const apiEndpoint = apiUrl + "/auth";

export function login(email, password) {
  return http.post(apiEndpoint + "/login", { email, password });
}
متد login، کار ارسال ایمیل و کلمه‌ی عبور کاربر را به اکشن متد Login کنترلر Auth، انجام می‌دهد و خروجی آن یک Promise است. برای استفاده‌ی از آن به کامپوننت src\components\loginForm.jsx بازگشته و متد doSubmit آن‌را به صورت زیر تکمیل می‌کنیم:
import * as auth from "../services/authService";

class LoginForm extends Form {
  state = {
    data: { username: "", password: "" },
    errors: {}
  };

  // ...

  doSubmit = async () => {
    try {
      const { data } = this.state;
      const {
        data: { access_token }
      } = await auth.login(data.username, data.password);
      console.log("JWT", access_token);
      localStorage.setItem("token", access_token);
      this.props.history.push("/");
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors };
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };
توضیحات:
- ابتدا تمام خروجی‌های ماژول authService را با نام شیء auth دریافت کرده‌ایم.
- سپس در متد doSubmit، اطلاعات خاصیت data موجود در state را که معادل فیلدهای فرم لاگین هستند، به متد auth.login برای انجام لاگین سمت سرور، ارسال کرده‌ایم. این متد چون یک Promise را باز می‌گرداند، باید await شود و پس از آن متد جاری نیز باید به صورت async معرفی گردد.
- همانطور که عنوان شد، خروجی نهایی متد auth.login، یک شیء JSON دارای خاصیت access_token است که در اینجا از خاصیت data خروجی نهایی دریافت شده‌است.
- سپس نیاز است برای استفاده‌های آتی، این token دریافتی از سرور را در جایی ذخیره کرد. یکی از مکان‌های متداول اینکار، local storage مرورگرها است (اطلاعات بیشتر).
- در آخر کاربر را توسط شیء history سیستم مسیریابی برنامه، به صفحه‌ی اصلی آن هدایت می‌کنیم.
- در اینجا قسمت catch نیز ذکر شده‌است تا خطاهای حاصل از return BadRequestهای دریافتی از سمت سرور را بتوان ذیل فیلد نام کاربری نمایش داد. روش کار آن، دقیقا همانند روشی است که برای فرم ثبت یک کاربر جدید استفاده کردیم.

اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، توکن دریافتی، در کنسول توسعه دهندگان مرورگر لاگ شده و سپس کاربر به صفحه‌ی اصلی برنامه هدایت می‌شود. همچنین این token ذخیره شده را می‌توان در ذیل قسمت application->storage آن نیز مشاهده کرد:



لاگین خودکار کاربر، پس از ثبت نام در سایت

پس از ثبت نام یک کاربر در سایت، بدنه‌ی response بازگشت داده شده‌ی از سمت سرور، همان شیء user است که اکنون Id او مشخص شده‌است. بنابراین اینبار جهت ارائه‌ی token از سمت سرور، بجای response body، از یک هدر سفارشی در فایل Controllers\UsersController.cs استفاده می‌کنیم (کدهای کامل آن در انتهای بحث پیوست شده‌است):
var jwt = _tokenFactoryService.CreateAccessToken(user);
this.Response.Headers.Add("x-auth-token", jwt);



در ادامه در کدهای سمت کلاینت src\components\registerForm.jsx، برای استخراج این هدر سفارشی، اگر شیء response دریافتی از سرور را لاگ کنیم:
const response = await userService.register(this.state.data);
console.log(response);
یک چنین خروجی را به همراه دارد که در آن، هدر سفارشی ما درج نشده‌است و فقط هدر content-type در آن مشخص است:


برای اینکه در کدهای سمت کلاینت بتوان این هدر سفارشی را خواند، نیاز است هدر مخصوص access-control-expose-headers را نیز به response اضافه کرد:
var jwt = _tokenFactoryService.CreateAccessToken(data);
this.Response.Headers.Add("x-auth-token", jwt);
this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
به این ترتیب وب سرور برنامه، هدر سفارشی را که قرار است برنامه‌ی کلاینت به آن دسترسی پیدا کند، مجاز اعلام می‌کند. اینبار اگر خروجی دریافتی از Axios را لاگ کنیم، در لیست هدرهای آن، هدر سفارشی x-auth-token نیز ظاهر می‌شود:


اکنون می‌توان این هدر سفارشی را در متد doSubmit کامپوننت RegisterForm، از طریق شیء response.headers خواند و در localStorage ذخیره کرد. سپس کاربر را توسط شیء history سیستم مسیریابی، به ریشه‌ی سایت هدایت نمود:
class RegisterForm extends Form {
  // ...

  doSubmit = async () => {
    try {
      const response = await userService.register(this.state.data);
      console.log(response);
      localStorage.setItem("token", response.headers["x-auth-token"]);
      this.props.history.push("/");
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors }; // clone an object
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-26-backend.zip و sample-26-frontend.zip
مطالب
شروع به کار با DNTFrameworkCore - قسمت 2 - طراحی موجودیت‌های سیستم
در قسمت قبل، امکانات این زیرساخت را ملاحظه کردیم. در این مطلب و مطالب آینده، روش طراحی بخش‌های مختلف یکسری سیستم فرضی را با استفاده از امکانات مذکور و با جزئیات بیشتر، بررسی خواهیم کرد.
به منظور اعمال خودکار یکسری مفاهیم توسط زیرساخت، نیاز است موجودیت‌های شما قراردادهای مورد نظر را پیاده سازی کرده باشند یا اینکه از موجودیت‌های پایه که آن قراردادها را پیاده سازی کرده‌اند، به عنوان میانبر، از آنها ارث بری کنید. برای دسترسی به این موجودیت‌های پایه و یکسری واسط که به عنوان قراردادهایی در بخش‌های مختلف این زیرساخت استفاده می‌شوند، نیاز است تا ابتدا بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0

مثال اول: یک موجودیت ساده بدون نیاز به مباحث ردیابی تغییرات

public class MeasurementUnit : Entity<int>, IAggregateRoot
{
   public const int MaxTitleLength = 50;
   public const int MaxSymbolLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Symbol { get; set; }
    public byte[] RowVersion { get; set; }
}

‎کلاس جنریک Entity، در برگیرنده یکسری اعضای مشترک بین سایر موجودیت‌های سیستم از جمله Id و TrackingState (به منظور سناریوهای Master-Detail)، می‌باشد. 

‎نکته: در این زیرساخت برای پیاده سازی CrudService برای یک موجودیت خاص، نیاز است تا واسط IAggregateRoot را نیز پیاده سازی کرده باشد. برای پیاده سازی واسط مذکور نیاز است تا خصوصیت RowVersion را به منظور مدیریت Optimistic مباحث همزمانی، به کلاس بالا اضافه کنیم. این موضوع برای موجودیت‌های وابسته به یک Aggregate ضروری نیست، چرا که آنها با AggregateRoot ذخیره خواهند شد و تراکنش جدایی برای ثبت، ویرایش و یا حذف آنها وجود ندارد.

مثال دوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت و آخرین ویرایش

public class Blog : TrackableEntity<long>, IAggregateRoot
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
}

کلاس جنریک TrackableEntity علاوه بر خصوصیات Id و TrackingState، یکسری خصوصیت دیگر از جمله زمان ثبت، زمان آخرین ویرایش، شناسه کاربر ثبت کننده، شناسه آخرین کاربر ویرایش کننده، اطلاعات مرورگرهای آنها و ... را نیز دارا می‌باشد. این خصوصیات به صورت خودکار توسط زیرساخت مقداردهی خواهند شد.


مثال سوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت، آخرین ویرایش و حذف نرم

public class Blog : FullTrackableEntity<long>, IAggregateRoot
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
}

کلاس جنریک FullTrackableEntity علاوه بر خصوصیات ذکر شده در مثال دوم، یکسری خصوصیت دیگر از جمله IsDeleted، شناسه کاربر حذف کننده، زمان حذف و ... را نیز دارا می‌باشد. همچنین مباحث فیلتر خودکار رکوردهای حذف شده، به صورت خودکار توسط زیرساخت انجام می‌گیرد که امکان غیرفعال کردن آن در شرایط مورد نیاز نیز وجود دارد.

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

public class Blog : Entity<long>, IAggregateRoot, ITenantEntity
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;
    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
    public long TenantId { get; set; }
}

با پیاده سازی واسط ITenantEntity، به صورت خودکار خصوصیت TenantId آن با توجه به اطلاعات مستاجر جاری سیستم مقداردهی خواهد شد و همچنین فیلتر خودکار بر روی رکوردهای مستاجرهای مختلف، توسط زیرساخت انجام می‌شود که این مکانیزم هم قابلیت غیرفعال شدن در شرایط خاص را دارد.

مثال پنجم: یک موجودیت به همراه تعدادی موجودیت جزئی (سناریوهای Master-Detail)

public class Invoice : TrackableEntity<long>, IAggregateRoot
{
    public InvoiceStatus Status { get; set; }
    public decimal TotalNet { get; set; }
    public decimal Total { get; set; }
    public decimal PayableTotal { get; set; }
    public decimal Debit { get; set; }
    public decimal Credit { get; set; }
    public decimal Gratuity { get; set; }
    public byte[] RowVersion { get; set; }

    public ICollection<InvoiceItem> Items { get; set; }
}

public class InvoiceItem : TrackableEntity
{
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Price { get; set; }
    public decimal UnitPriceDiscount { get; set; }

    public long ItemId { get; set; }
    public Item Item { get; set; }
    public long InvoiceId { get; set; }
    public Invoice Invoice { get; set; }
}

همانطور که مشخص می‌باشد، موجودیت وابسته یا همان Detail، نیاز به پیاده سازی IAggregateRoot را نخواهد داشت. همانطور که اشاره شد، تراکنش مجزایی برای این موجودیت‌ها نخواهیم داشت و درون تراکنش AggregateRoot، عملیات CRUD آنها انجام خواهد شد و برای انجام عملیات ویرایش، به همراه Root متناظر با خود، واکشی خواهند شد. این موضوع یکی از نقاط قوت زیرساخت محسوب می‌شود که در مقالات آینده و در قسمت طراحی سرویس‌های متناظر با موجودیت‌های سیستم، با جزئیات بیشتری بررسی خواهد شد.

مثال ششم: یک موجودیت با امکان شماره گذاری خودکار

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Number { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public byte[] RowVersion { get; set; }
}

همانطور که در مطلب «طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید» ملاحظه کردید، نیاز است تا موجودیت مورد نظر، پیاده ساز واسط INumberedEntity نیز باشد. این واسط دارای خصوصیت رشته‌ای Number می‌باشد و همچنین زیرساخت به صورت خودکار در زمان ثبت، این خصوصیت را برای موجودیت‌هایی از این نوع، با رعایت مباحث همزمانی مقداردهی می‌کند.

مثال هفتم: یک موجودیت با امکان ذخیره سازی اطلاعات اضافی در قالب فیلد JSON

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity, IExtendableEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Number { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public byte[] RowVersion { get; set; }

    public string ExtensionJson { get; set; }
}

با پیاده سازی واسط IExtendableEntity، یکسری متد الحاقی برروی اشیاء موجودیت مورد نظر فعال خواهند شد که امکان مقداردهی یا خواندن این اطلاعات اضافی را خواهید داشت. به عنوان مثال:

var task = new Task();
task.SetExtensionValue("Name","Value");
var value = task.ReadExtensionValue("Name");
//or any complex object as string json

با دو متد الحاقی استفاده شده در بالا، امکان مقداردهی، تغییر و خواندن مقدار خصوصیت‌های اضافی را خواهیم داشت که نیاز است موجودیت مورد نظر در دل خود نگهداری کند ولی ارزش و اهمیت زیادی در Domain ندارند.


مثال هشتم: طراحی یک نوع شمارشی (Enum)

public class OrderStatus : Enumeration
{
    public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
    public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
    public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
    public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
    public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
    public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());

    protected OrderStatus()
    {
    }

    public OrderStatus(int id, string name)
        : base(id, name)
    {
    }
}

برای سناریوهایی که صرفا قصد انتخاب یک یا چند (حالت enum flags) مورد از بین یک لیست مشخص و سپس ذخیره سازی آنها را دارید، استفاده از نوع داده enum کفایت می‌کند؛ ولی اگر قصد استفاده از آنها برای flow control را دارید، در این صورت به طراحی شکننده‌ای خواهید رسید که پر شده است از if/else هایی که مقادیر مختلف enum مورد نظر را بررسی می‌کنند. با استفاده از کلاس Enumeration امکان مدل کردن انوع شمارشی که مرتبط هستند با منطق تجاری سیستم را با راه حل شیء گرا خواهید داشت. در این صورت رفتارهای متناظر با هریک از فیلدهای یک نوع شمارشی می‌تواند به عنوان رفتاری در دل خود کپسوله شده باشد و اینبار داده و رفتار کنار هم خواهند بود. 

نکته: برای مطالعه بیشتر می‌توانید به مطالب ^ و ^ مراجعه کنید.

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

نیاز به حذف نرم بدون نگهداری اطلاعات ردیابی تغییرات

public interface ISoftDeleteEntity
{
    bool IsDeleted { get; set; }
}

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

نیاز به مقداردهی خودکار زمان ثبت یک موجودیت خاص 

این امر با پیاده سازی واسط زیر امکان پذیر خواهد بود.

public interface IHasCreationDateTime
{
    DateTimeOffset CreationDateTime { get; set; }
}

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

مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت هشتم - ساده سازی معرفی سرویس‌ها توسط Scrutor
قابلیت‌های قرار گرفته‌ی در اسمبلی Microsoft.Extensions.DependencyInjection که پایه‌ی تزریق وابستگی‌های برنامه‌های مبتنی بر NET Core. را ارائه می‌دهد، برای پیاده سازی اکثر پروژه‌ها کافی است. اما اگر از نگارش‌های پیشین ASP.NET MVC به ASP.NET Core مهاجرت کرده باشید، حتما با قابلیت‌های ویژه‌ی اسکن اسمبلی‌های موجود در IoC Containers ثالث، جهت ساده سازی معرفی سرویس‌های برنامه به سیستم تزریق وابستگی‌ها، آشنایی دارید. برای مثال StructureMap قابلیت اسکن اسمبلی‌های موجود در برنامه و معرفی اینترفیس‌ها و سرویس‌های موجود در آن‌را به Container خود دارد:
var container = new Container(x =>
            {
                x.Scan(scanner =>
                {
                    scanner.AssemblyContainingType<IOrderHandler>();
                    // connects `IAccounting` to `Accounting` and `ISales` to `Sales` automatically.
                    scanner.WithDefaultConventions();
                });
            });
و یا AutoFac نیز به همین صورت:
builder.RegisterAssemblyTypes(myAssembly)
    .Where(t => t.IsAssignableTo<IMyInterface>())
    .AsImplementedInterfaces();
البته می‌توان مجددا به تمام این قابلیت‌ها رسید؛ به شرطی‌که سیستم تزریق وابستگی‌های پایه‌ی NET Core. را با یکی از IoC Containers ثالث به طور کامل تعویض کنیم. اگر قصد چنین تعویض پایه‌ای را ندارید و هنوز قصد دارید از همان Microsoft.Extensions.DependencyInjection استفاده کنید، اما تعدادی متد الحاقی جدید تعریف شده‌ی بر فراز آن، کار اسکن کردن اسمبلی‌ها را مانند قبل انجام دهند، می‌توان از کتابخانه‌ی کمکی Scrutor استفاده کرد. این کتابخانه، جایگزین سیستم تزریق وابستگی‌های توکار برنامه‌های NET Core. نیست؛ بلکه صرفا مکمل آن است.


دریافت و نصب کتابخانه‌ی کمکی Scrutor

کتابخانه‌ی کمکی Scrutor سورس باز بوده و بسته‌ی NuGet آن توسط یکی از دستورات زیر به پروژه افزوده می‌شود:
> Install-Package Scrutor
> dotnet add package Scrutor
و یا به صورت مدخلی جدید در فایل csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Scrutor" Version="3.0.2" />
  </ItemGroup>
</Project>


ثبت و معرفی ساده‌تر سرویس‌ها بر اساس قواعد نامگذاری آن‌ها توسط Scrutor

فرض کنید تعدادی سرویس را به صورت زیر تعریف کرده‌اید:
namespace CoreIocServices
{
    public interface IFoo
    {
        void Run();
    }

    public class Foo : IFoo
    {
        public void Run()
        {
            throw new System.NotImplementedException();
        }
    }

    public interface IBar
    {
        void Add();
    }

    public class Bar : IBar
    {
        public void Add()
        {
            throw new System.NotImplementedException();
        }
    }


    public interface IBaz
    {
        void Stop();
    }

    public class Baz : IBaz
    {
        public void Stop()
        {
            throw new System.NotImplementedException();
        }
    }
}
روش متداول معرفی آن‌ها به IoC Container برنامه به صورت زیر است:
services.AddScoped<IFoo, Foo>();
services.AddScoped<IBar, Bar>();
services.AddScoped<IBaz, Baz>();
و هرچقدر تعداد سرویس‌های برنامه بیشتر شود، سطرهای فوق نیز بیشتر خواهند شد.
در اینجا در حین تعریف سرویس‌های فوق این روش نامگذاری رعایت شده‌است: هر اینترفیس، نامش یک I بیشتر از نام کلاس مشتق شده‌ی از آن دارد؛ مانند اینترفیس IFoo و کلاس Foo. کتابخانه‌ی StructureMap که در ابتدای بحث معرفی شد، کار اسکن و اتصال یک چنین سرویس‌هایی را با تعریف scanner.WithDefaultConventions انجام می‌دهد. معادل آن با Scrutor به صورت زیر است:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Scan(scan =>
                //scan.FromCallingAssembly()
                scan.FromAssemblyOf<IFoo>()
                    .AddClasses()
                    .AsMatchingInterface()
                    .WithScopedLifetime());
تعریف فوق به این معنا است:
- scan.FromAssemblyOf کار اسکن اسمبلی را انجام می‌دهد که نوع IFoo در آن قرار دارد. اگر از scan.FromCallingAssembly استفاده کنیم، به این معنا است که کار اسکن را دقیقا از همین اسمبلی فراخوان کدهای جاری، شروع کن. اما چون IFoo تعریف شده، در یک پروژه و اسمبلی دیگر قرار دارد، به همین جهت نیاز به ذکر صریح اسمبلی آن نیز هست.
- AddClasses یعنی تمام کلاس‌های public, non-abstract را به لیست services اضافه کن.
- AsMatchingInterface یعنی بر اساس قرارداد نامگذاری IClassName و ClassName، اتصالات سرویس‌ها را انجام بده.
بجای آن می‌توان از AsImplementedInterfaces نیز استفاده کرد. این حالت برای زمانی مناسب است که یک کلاس، چندین اینترفیس را پیاده سازی کند (مثلا کلاس TestService اینترفیس‌های ITestService و IService را پیاده سازی کرده باشد) و علاقمند باشید به ازای هر اینترفیس، یکبار سرویس آن نیز ثبت شود؛ کاری مانند تنظیمات زیر:
services.AddScoped<ITestService, TestService>();
services.AddScoped<IService, TestService>();
یا حتی می‌توان از متد ()<As<T نیز استفاده کرد. در اینجا به Scrutor گفته می‌شود که تمام کلاس‌های یافت شده را بر اساس نوع سرویس T ثبت و معرفی کن. البته اگر کلاسی نتواند نوع اینترفیس T را پیاده سازی کند، در زمان اجرا با استثناء مواجه خواهید شد.
- WithScopedLifetime نیز طول عمر این سرویس‌های اضافه شده را مشخص می‌کند. در اینجا می‌توان WithTransientLifetime و WithSingletonLifetime را نیز ذکر کرد.

بنابراین همانطور که ملاحظه می‌کنید، هنوز هم همان سیستم Microsoft.Extensions.DependencyInjection برقرار است؛ اما با وجود متد الحاقی جدید Scan، کار تعاریف سرویس‌های برنامه به شدت ساده می‌شود.


کار با وهله‌های کلاس‌های سرویس‌ها بجای اینترفیس‌های آن توسط Scrutor

می‌خواهیم مثال سوم قسمت ششم «چگونه بجای اینترفیس‌ها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگی‌ها درخواست کنیم؟» را توسط Scrutor پیاده سازی کنیم:
namespace CoreIocServices
{
    public interface IService { }
    public class Service1 : IService { }
    public class Service2 : IService { }
    public class Service : IService { }
}
در حالت متداول آن می‌توان از روش زیر نیز استفاده کرد:
services.AddTransient<Service1>();
services.AddTransient<Service2>();
services.AddTransient<Service>();
که با افزایش تعداد کلاس‌های سرویس برنامه به همین نحو نیز افزایش خواهند یافت. معادل این تنظیمات با Scrutor به صورت زیر است:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Scan(scan =>
              //scan.FromCallingAssembly()
              scan.FromAssemblyOf<IService>()
                  .AddClasses()
                  .AsSelf()
                  .WithTransientLifetime());
در اینجا اسمبلی حاوی IService اسکن خواهد شد و سپس تمام کلاس‌های public, non-abstract آن AsSelf (ثبت پیاده سازی خود کلاس به عنوان سرویس) با طول عمر Transient به لیست services اضافه می‌شوند و یا اگر صرفا تعدادی سرویس مشخص مد نظر بود می‌توان به صورت زیر عمل کرد:
services.Scan(scan =>
               scan.AddTypes(new[] { typeof(Service1), typeof(Service2) })
                   .AsSelf()
                   .WithTransientLifetime());
متدهایی که در Scrutor، یک پیاده سازی را به عنوان سرویس معرفی می‌کنند، شامل این موارد هستند:
AsSelf: معادل ()<services.AddTransient<TestService است. در این حالت کلاس‌هایی که اینترفیسی را پیاده سازی نمی‌کنند و یا در کل مایل هستید که از طریق تزریق وابستگی‌ها در دسترس باشند، می‌توان توسط متد AsSelf به سیستم معرفی کرد.
AsSelfWithInterfaces: معادل تنظیمات زیر است:
services.AddSingleton<TestService>();
services.AddSingleton<ITestService>(x => x.GetRequiredService<TestService>());
services.AddSingleton<IService>(x => x.GetRequiredService<TestService>());
فرض کنید کلاس TestService اینترفیس‌های ITestService و IService را پیاده سازی کرده باشد. با استفاده از AsSelfWithInterfaces، یکبار پیاده سازی خود سرویس به سیستم معرفی می‌شود، سپس به ازای هر اینترفیس، از همان وهله‌ی TestService برای وهله سازی سرویس‌های ITestService و IService نیز استفاده می‌شود.


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

Scrutor به همراه روش‌های متعددی برای تعریف اسمبلی یا اسمبلی‌هایی است که باید اسکن شوند و نمونه‌ای از آن‌را با FromAssemblyOf بررسی کردیم:
services.Scan(scan =>
              //scan.FromCallingAssembly()
              scan.FromAssemblyOf<IService>()
سایر موارد آن به شرح زیر هستند:
الف) FromAssemblyOf<>, FromAssembliesOf : اسمبلی یا اسمبلی‌هایی که نوع یا نوع‌های تعیین شده را به همراه دارند، اسکن می‌کند.
ب) FromCallingAssembly, FromExecutingAssembly, FromEntryAssembly کار اسکن اسمبلی‌های فراخوان، اسمبلی که هم اکنون در حال اجرا است و اسمبلی آغازین برنامه را انجام می‌دهند.
ج) FromAssemblyDependencies: تمام اسمبلی‌هایی را که وابسته‌ی به اسمبلی معرفی شده‌ی به آن هستند، اسکن می‌کند.
د) FromApplicationDependencies, FromDependencyContext: تمام اسمبلی‌هایی را که توسط برنامه، ارجاعی به آن‌ها وجود دارند، اسکن می‌کند.


انتخاب دقیق‌تر کلاس‌ها و سرویس‌های مدنظر توسط Scrutor

شاید عملکرد کلی متد AddClasses مدنظر شما نباشد و نیاز به انتخاب دقیق‌تری از سرویس‌های اسکن شده را داشته باشید؛ برای این مورد نیز Scrutor روش‌های زیر را ارائه می‌دهد. برای مثال خود کلاس AddClasses دارای overloadهای زیر نیز هست:
    public interface IImplementationTypeSelector : IAssemblySelector, IFluentInterface
    {
        IServiceTypeSelector AddClasses();
        IServiceTypeSelector AddClasses(bool publicOnly);
        IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action);
        IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action, bool publicOnly);
    }
حالت پیش‌فرض آن انتخاب تمام کلاس‌های public, non-abstract است. اگر پارامتر publicOnly را با false مقدار دهی کنید، internal/private nested classes را نیز انتخاب می‌کند. پارامتر action ای که در اینجا درنظر گرفته شده، جهت فیلتر کردن سرویس‌های انتخابی است که تعدادی از مثال‌های آن‌را در زیر بررسی می‌کنیم:
services.Scan(scan => scan
              .FromAssemblyOf<IService>()
                .AddClasses(classes => classes.AssignableTo<IService>())
// .AddClasses(classes => classes.InNamespaces("MyApp")) 
// .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Repository")) 
                    .AsImplementedInterfaces()
                    .WithTransientLifetime());
در اینجا در حالت اول، کلاس‌هایی که صرفا اینترفیس IService را پیاده سازی کرده باشند، انتخاب می‌شوند. حالت دوم آن، انتخاب‌ها را به یک فضای نام محدود می‌کند و حالت سوم اگر نام کلاسی به Repository ختم شود، آن‌را به عنوان سرویس انتخاب خواهد کرد.


مدیریت جایگزینی سرویس‌ها توسط Scrutor

یکی از مزیت‌های طراحی یک برنامه با درنظر گرفتن الگوی تزریق وابستگی‌ها، امکان جایگزین کردن سرویس‌های پیش‌فرض آن با سرویس‌های دیگری است. فرض کنید کتابخانه‌ای ارائه شده و از الگوریتم هش کردن X استفاده کرده‌است؛ اما شما علاقمندید تا از الگوریتم Y بجای آن استفاده کنید. اگر این کتابخانه وهله‌ی الگوریتم هش کردن را از طریق تزریق وابستگی‌ها تامین کرده باشد، فقط کافی است در ابتدای معرفی تنظیمات تزریق وابستگی‌های آن، سرویس الگوریتم هش کردن موجود را با نمونه‌ی خاص خودتان جایگزین کنید.
اکنون فرض کنید پیش از استفاده‌ی از Scrutor، تعدادی سرویس را به روش متداولی ثبت و معرفی کرده‌اید:
services.AddTransient<ITransientService, TransientService>();
services.AddScoped<IScopedService, ScopedService>();
حال که قرار است متد Scan آن، سرویس‌های یک اسمبلی را به لیست موجود اضافه کند، به سرویس‌های زیر می‌رسد:
public class TransientService : IFooService {}
public class AnotherService : IScopedService {}
 رفتار آن با سرویس‌های معادلی که از پیش ثبت شده‌اند چگونه باید باشد؟ برای مدیریت این مساله، متد UsingRegistrationStrategy پیش بینی شده‌است:
services.Scan(scan =>
                scan.FromAssemblyOf<IFoo>()
                    .AddClasses()
                    .UsingRegistrationStrategy(RegistrationStrategy.Skip)
                    .AsMatchingInterface()
                    .WithScopedLifetime());
و پارامتر دریافتی آن یک چنین امضایی را دارد:
namespace Scrutor
{
    public abstract class RegistrationStrategy
    {
        public static readonly RegistrationStrategy Skip;
        public static readonly RegistrationStrategy Append;
        protected RegistrationStrategy();
        public static RegistrationStrategy Replace();
        public static RegistrationStrategy Replace(ReplacementBehavior behavior);
        public abstract void Apply(IServiceCollection services, ServiceDescriptor descriptor);
    }
}
- حالت Append آن که حالت پیش‌فرض نیز هست، تمام سرویس‌های یافت شده را به لیست IServiceCollection اضافه می‌کند؛ صرفنظر از اینکه پیشتر ثبت شده‌است یا خیر.
- حالت Skip آن، سرویسی را تکراری ثبت نمی‌کند. یعنی اگر سرویسی پیشتر در مجموعه‌ی IServiceCollection موجود بود، مجددا آن‌را ثبت نمی‌کند.

سپس نوبت به متدهای Replace می‌رسد که یک چنین پارامتری را قبول می‌کنند:
namespace Scrutor
{
    [Flags]
    public enum ReplacementBehavior
    {
        Default = 0,
        ServiceType = 1,
        ImplementationType = 2,
        All = 3
    }
}
- در حالت استفاده‌ی از Replace(​ReplacementBehavior.​ServiceType)، اگر سرویسی پیشتر در لیست IServiceCollection ثبت شده باشد، آن‌را حذف کرده و سپس نمونه‌ی جدید را ثبت می‌کند (ثبت سرویس بر اساس اینترفیس و پیاده سازی آن).
- در حالت استفاده‌ی از Replace(​ReplacementBehavior.​ImplementationType)، اگر پیاده سازی کلاسی پیشتر در لیست IServiceCollection ثبت شده باشد، آن‌را حذف کرده و سپس نمونه‌ی جدید را ثبت می‌کند (ثبت سرویس صرفا بر اساس نام کلاس آن).
- حالت Replace(​ReplacementBehavior.All) هر دو حالت قبل را با هم شامل می‌شود.


امکان ترکیب چندین استراتژی جستجو با هم توسط Scrutor

در یک برنامه‌ی واقعی غیرممکن است که بخواهید تمام کلاس‌ها را با یک طول عمر، اسکن و ثبت کنید. برای این منظور می‌توان از قابلیت فیلتر کردن کلاس‌ها که در مورد آن بحث شد و همچنین امکان ترکیب زنجیر وار حالت‌های مختلف اسکن، استفاده کرد:
services.Scan(scan => scan 
  .FromAssemblyOf<CombinedService>() 
    .AddClasses(classes => classes.AssignableTo<ICombinedService>()) // Filter classes 
      .AsSelfWithInterfaces() 
      .WithSingletonLifetime() 
 
    .AddClasses(x=> x.AssignableTo(typeof(IOpenGeneric<>))) // Can close generic types 
      .AsMatchingInterface() 
 
    .AddClasses(x=> x.InNamespaceOf<MyClass>()) 
      .UsingRegistrationStrategy(RegistrationStrategy.Replace()) // Defaults to ReplacementBehavior.ServiceType 
      .AsMatchingInterface() 
      .WithScopedLifetime() 
 
  .FromAssemblyOf<DatabaseContext>()   // Can load from multiple assemblies within one Scan() 
    .AddClasses()  
      .AsImplementedInterfaces() 
);