مطالب
چک لیست شروع به ساخت یک نرم افزار بزرگ یا متوسط
 کتابها و منابع آموزشی بسیاری در جهت یادگیری برنامه سازی و مهندسی نرم­ افزار وجود دارند که اکثراً هم مطالب مفید و بسیار خوبی را ارائه می­دهند؛ با این‌حال یکی از سؤالات بزرگی که بعد از مطالعه آنها در ذهن افراد ممکن است پیش بیاید این است که با خود می­پرسند حالا چه کنم؟ از کجا شروع کنم؟ در واقع ذهن افراد پر است از اطلاعات تخصصی بسیار مفید ولی نمیدانند آنها را چگونه سرهم بندی کنند تا یک سیستم نرم­ افزاری قابل اتکا تولید کنند. توسعه گران با تجربه با گذشت زمان، مطالعه کد نرم­ افزارهای موجود، مطالعه مضاعف، شرکت در بحثهای تخصصی و ... معمولاً می­دانند که باید از کجا شروع کنند. در اینجا بنده سعی کرد‌ه‌ام مواردی را که توسعه گران باتجربه در شروع ساخت یک نرم ­افزار متوسط یا بزرگ با رویکرد توسعه برای وب در مورد آنها تصمیم می­گیرند، به صورت مختصر توضیح دهم. طبیعی هست که ممکن است این لیست کامل نباشد، نظرات دوستان میتواند آنرا کاملتر کند.

در اینجا غیر از مورد زمانبندی انجام پروژه سعی شده است به دیگر موارد غیره از قبیل شناخت نیازمندیها، نحوه بستن قرارداد، نحوه قیمت دهی و ... اشاره نشود.

 

در ابتدا در مورد موضوعات کلی و عمومی بحث می‌کنیم.

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

2- زمانبندی انجام پروژه، به نظر خود بنده، سخت‌ترین و اساسی‌ترین مرحله، برای هر پروژه‌­ای، زمانبندی مناسب آن است که نیازمندی اساسی آن، شناخت سایر مواردی است که در این متن بدان‌ها اشاره می­شود. زمانبندی دقیق، قرار ملاقاتها و تحویل به‌موقع پیش نمایشهای نرم­ افزار، ارتباط مستمر با کارفرما و تحویل حتی زودتر از موعد پروژه باعث رضایت بیشتر کارفرما و حس اطمینان بیشتر خواهد شد. اگر در تحویل پروژه دیرکرد وجود داشته باشد، باعث دلسردی کارفرما و نوعی تبلیغ منفی خواهد بود. حتی زمانبندی و تحویل به موقع پروژه برای کارفرما بیشتر از کیفیت اهمیت دارد.

3- انتخاب معماری نرم­ افزار، معماری نرم ­افزار در اصل تعیین کننده نحوه قطعه بندی و توزیع تکه‌های نرم افزار، نحوه ارتباط اجزاء،، قابلیت تست پذیری، قابلیت نگهداری و قابلیت استفاده مجدد از کدهای تولید شده می­باشد. یکی از اهداف اساسی‌ای که باید در معماری نرم­‌افزار بدان توجه کرد، قابلیت استفاده مجدد از کد است. در یک معماری خوب ما قطعاتی درست خواهیم کرد که به‌راحتی می­توانیم از آن در نرم‌افزارهای دیگر نیز استفاده کنیم. البته قابلیت تست پذیری و قابلیت نگهداری نیز حداقل به همان اندازه اهمیت دارند. در این سایت موارد بسیار زیاد و کاملی جهت ساخت معماری مناسب و design patterns وجود دارد که می­توانید در اینجا یا اینجا مشاهده کنید.

 4- قابلیت اجرا بر روی پلتفرمهای مختلف، هرچند این مورد ممکن است بیشتر به نظر کارفرما بستگی داشته باشد، اما در کل اگر کارفرما بتواند سیستم را در پلتفرمهای مختلفی اجرا کند، راضی‌تر خواهد شد. اگر قصد فروش نرم‌­افزار طراحی شده را داشته باشیم، در اینصورت نیز می­توانیم کاربران پلتفرمهای مختلف را مورد هدف قرار دهیم یا سیستم را در سرورهای مختلفی میزبانی کنیم.

5- انتخاب سیستم بانک اطلاعاتی و نحوه ارتباط با آن. باید تصمیم بگیرید که از چند سیستم بانک اطلاعاتی، چگونه و به چه منظوری استفاده خواهید کرد. مواردی وجود دارند که سیستم را طوری طراحی کرد‌ه‌اند تا در زمان بهره برداری امکان انتخاب بانک‌های اطلاعاتی یا نحوه ذخیره اطلاعات برای مدیر سیستم وجود دارد. مثلا در BlogEngine.net میتوان انتخاب کرد که اطلاعات در SQL Server ذخیره شوند یا در سیستم فایل مبتنی بر XML . بحثهای بسیار زیادی در این سایت و کل فضای وب پیرامون نحوه انتخاب و استفاده از ORM ها، چگونگی معماری مناسب آن وجود دارد. بطور مثال همیشه بحث سر اینکه از الگوی Repository استفاده شود یا نشود وجود دارد! باید به خودمان پاسخ دهیم که آیا واقعاً نیاز است که سیستم را برای امکان استفاده از Orm‌های مختلف طراحی کنیم؟

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

7- میزان مشارکت دیگران در رفع نیازمندیهای کابران. ممکن است این گزینه در درجه اول زیاد با اهمیت جلوه ندهد، اما با تعمق در وبسایت‌ها و نرم‌­افزارهای بزرگ که هم اکنون در دنیا صاحب نامی شده‌اند می‌بینیم همه آنها تمهیداتی اندیشیده‌اند تا با وجود کپسوله کردن موارد پس زمینه، امکاناتی را در جهت مشارکت دیگران فراهم کنند. اکثر شبکه­‌های اجتماعی api هایی را مهیا کرده­ اند که افراد ثالث می­توانند از آنها استفاده کنند. اکثر سیستم‌های مدیریت محتوا و ابزارهای e-commerce تمهیداتی را برای راحتی ساخت plugin و api‌های برای راحتی برقراری ارتباط اشخاص ثالث اندیشیده‌اند. از نظر این جانب موارد 6 و 7 برای ادامه حیات و قابلیت رقابت پذیری پروژه از درجه اهمیت زیادی برخوردار است.

8- معماری Multi tenancy بلی یا خیر؟ Multi tenancy یک از بحثهای مهم رایانش ابری است. در این حالت فقط یک نمونه از نرم­ افزار در سمت سرور در حال اجراست ولی کاربر یا گروهی از کاربران دید یا تنظیمات متفاوتی از آن‌را دارند.

در ادامه به موارد فنی‌تری خواهیم پرداخت:

9- بحث انتخاب ابزار Dependency injection مناسب و مهیا سازی امکاناتی جهت هرچه راحت‌تر کردن امکان تنظیم و register کردن اشیا بدان. نحوه پیکربندی مناسب این مورد می­تواند کد نویسی را برایتان بسیار راحت کند. دات نت تیپس مطالب بسیاری را در این مورد ارائه داده است میتوانید اینجا را ببینید.

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

11- Logging. یک سیستم لاگر مناسب می­تواند وارنینگ‌ها و خطاهای بوجود آمده در سیستم را در یک رسانه ذخیره سازی حفظ کند و شما به عنوان توسعه دهنده می­توانید با مطالعه آن نسبت به رفع خطاهای احتمالی و بهبود در نسخه‌های آتی کمک بگیرید.

12- Audit logging یا Activity logging و Entity History. می­توانید کل یا برخی از فعالیتهای کاربر را در یک رسانه ذخیره سازی، ذخیره کنید، از قبیل زمان ورود و خروج، آی‌پی مورد استفاده، سیستم عامل، مرورگر، بازبینی از صفحه وغیره. همچنین در audit logging میتوانید زمانهای دقیق تغییرات مختلف موجود در موجودیتهای سیستم، فرد انجام دهنده تغییرات، سرویس انجام دهنده تغییرات، مدت زمان سپری شده و ... را ذخیره کرد. Entity History : ممکن است تصمیم بگیرید که کل اتفاقاتی را که برای یک موجودیت در طول زمان حیاتش در سیستم می‌افتد، ذخیره کنید.

13- Eventing ، Background Worker‌ها و Backgroudn jobs ( Scheduled tasks ). باید سیستم را طوری طراحی کرد که بتواند به تغییرات و اتفاقات افتاده در سیستم پاسخ دهد. همچنین این مورد یکی از نیازمندیهای معماری بر اساس پلاگین است. Background Worker‌ها در واقع کارهایی هستند که در پس زمینه انجام میشوند و نیازی نیست که کاربر برای اتمام آن منتظر بماند؛ مثلاً ارسال ایمیل خوش آمدگویی را میتوان با آن انجام داد. Background jobs کمی متفاوت هستند در واقع اینها فعالیتهای پس زمینه­ای هستند که ممکن است در فواصل زمانی مختلف اتفاق بیافتند، مثل پاکسازی کش در فواصل زمانی مناسب. در سیستمهای مختلف تمهیداتی برای ذخیره سازی فعالیتهایی که توسط background jobs انجام میشود اندیشیده می­شود.

14- پیکربندی صحیح نحوه ذخیره و بازیابی تنظیمات سیستم. در یک سیستم ممکن است شما تنظیمات متعددی را در اختیار کاربر و یا حتی خودتان قرار دهید. باید سیستم را طوری طراحی کنید که بتواند با راحت‌ترین و سریعترین روش ممکن به تنظیمات موجود دستیابی داشته باشد.

15- خطاهای کاربر را در نظر بگیریم، باید یادمان باشد کاربر ممکن الخطاست و ما برای رضایت مشتری و قابلیت اتکای هرچه بیشتر برنامه باید سیستم را طوری طراحی کنیم که امکان برگشت از خطا برای کاربر وجود داشته باشد. مثلاً در SoftDelete مواردی که حذف می­شوند در واقع به طور کامل از بانک اطلاعاتی حذف نمیشوند بلکه تیک حذف شده میخورند. پس امکان بازگردانی وجود خواهد داشت.

16- Mapping یا Object to object mapping. در توسعه شی‌‌ءگرا مخصوصاً در معماری‌هایی مثل MVC یا Domain driven در موارد بسیاری نیاز خواهید داشت که مقادیر اشیاء مختلفی را در اشیای دیگری کپی کنید. سیستمهای زیادی برای این کار موجود هستند. باید تلاش کرد ضمن اینکه یک سیستم مناسب انتخاب کنیم، باید تمهیدی بیاندیشیم که تنظیمات آن شامل کد نویسی هرچه کمتری باشد.

17- Authorization یا تعیین هویت. باید با مطالعه و بررسی، سیستم و ابزار مناسبی را برای هویت سنجی اعضاء، تنظیم نقشها و دسترسی‌های کاربران انتخاب کرد. باید امکان عضویت از طریق شبکه‌های اجتماعی مختلف را مورد بررسی قرار داد.

18- سرویس‌های Realtime. کاربری یکی از مطالب شما را می­‌پسندد و شما نوتیفیکشن آنرا سریع در صفحه‌­ای که باز کردید می­بینید. این یک مورد بسیار کوچکی از استفاده از سرویسهای realtime هست. ابزارهای مختلفی برای زبانها و فریم‌ورکهای مختلف وجود دارند؛ مثلاً میتوانید اینجا را مطالعه کنید.

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

20- استفاده از منابع ابری یا توزیع شده، امروزه برای بسیاری از کارها تمهیداتی از طرف شرکتهای بزرگ به صورت رایگان و یا غیر رایگان اندیشیده شده است که به راحتی می­توان از آنها استفاده کرد. برای نمونه میتوان از سرویسهای Email به عنوان ساده‌ترین و معمول‌ترین این سیستمها یاد کرد. اما امروزه شرکتها حتی امکاناتی جهت ذخیره سازی داده‌های blob (مجموعه ای از بایتها با حجم زیاد) را ارائه می­دهند؛ امکانات دیگری نظیر کم کردن حجم تصاویر، تبدیل انواع mime type‌ها و ...

21- امنیت، فریم‌ورکها اغلب موارد امنیتی پایه‌ای را به صورت مطلوب یا نسبتا مطلوبی رعایت می­کنند؛ ولی با این‌حال باید در مورد امنیت سیستمی که توسعه می‌دهیم مطالعه داشته باشیم و موارد امنیتی ضروری را رعایت کنیم و همیشه مواظب باشیم که آنها را رعایت کنیم.
مطالب
مرور چند تجربه کوتاه با Microsoft virtual pc

از virtual pc که یک ویندوز اکس پی بر روی آن نصب کرده‌ام برای اتصال به VPN‌ استفاده می‌کنم. (متاسفانه آخرین نگارش vmware برای این‌کار جواب نداد)
زمان اتصال به VPN کل سیستم وارد شبکه مورد نظر خواهد شد و این مورد شاید به‌دلایلی برای مثال قطع اینترنت و یا اعمال پالیسی‌های شبکه بر روی کامپیوتر کاری جالب نباشد (استفاده از VPN برای اتصال به یک شبکه دومین ویندوزی). اما با نصب ماشین مجازی و اجرای یک سیستم عامل دیگر به موازات سیستم عامل اصلی، کار اتصال به VPN از داخل ماشین مجازی صورت خواهد گرفت و تمام این اعمال هم از سیستم عامل مادر مجزا و ایزوله خواهند بود.
یک ویندوز اکس پی با اختصاص 200 مگ رم هم کار می‌کند و عملا باری را بر روی سیستم عامل مادر تحمیل نخواهد کرد. همچنین حتما از منوی action گزینه install or update virtual machine additions را انتخاب کنید تا کارآیی سیستم عامل مجازی را بهبود بخشید. حداقل فایده آن این است که اشاره‌گر ماوس را به سادگی می‌توان از ماشین مجازی خارج کرد و هر بار نیازی به فشردن دکمه‌ ALT سمت راست نخواهد بود!
اولین مشکلی که هنگام کار با یک ماشین مجازی خود نمایی می‌کند بحث انتقال فایل بین سیستم عامل مادر (ویندوزی که شما ماشین مجازی را روی آن نصب کرده‌اید) و ماشین مجازی است. عمومی‌ترین راه، ایجاد یک فایل iso از فایل‌های مورد نظر است و سپس انتخاب منوی CD و گزینه capture iso image . این روش بر روی vmware هم جواب می‌دهد (معرفی فایل iso بعنوان CD-ROM آن). خوشبختانه عمل drag & drop (از سیستم عامل مادر به ماشین مجازی) که شاید در وحله اول به ذهن نرسد اینجا بخوبی کار خواهد کرد و مشکل ساخت فایل‌های iso را برطرف می‌کند. (البته vmware کمی پیشرفته‌تر است و حتی copy و Paste را نیز پشتیبانی می‌کند. اما خوب، رایگان نیست!)
مشکل بعدی با ms virtual pc افزایش تدریجی حجم آن است. روز اول 2 گیگ، روز سوم 4 گیگ، هفته بعد می‌شود 6 گیگ! برای فشرده سازی آن می‌توان به روش زیر عمل کرد:
به مسیر زیر مراجعه کنید: (اگر پیش‌فرض‌های نصب را پذیرفته‌اید)
C:\Program Files\Microsoft Virtual PC\Virtual Machine Additions
فایل Virtual Disk Precompactor.iso را از طریق منوی CD و گزینه capture iso image باز کنید. برنامه‌ای به صورت خودکار اجرا خواهد شد که سیستم عامل مجازی را آماده فشرده سازی می‌کند. پس از پایان کار، سیستم عامل مجازی را خاموش کنید. سپس به منوی file گزینه virtual disk wizard مراجعه نمائید. در صفحه بعدی گزینه ویرایش یک ماشین مجازی موجود را انتخاب کرده و فایل ماشین مجازی مورد نظر را که پیشتر برای فشرده سازی آماده کردیم به آن معرفی کنید. در صفحه بعد گزینه compact it را انتخاب کرده و در ادامه می‌توانید مسیر جدیدی را مشخص کنید یا انتخاب کنید که فایل نهایی فشرده شده جایگزین فایل موجود شود.
با اینکار یک ماشین مجازی 6 گیگابایتی به 3 گیگ کاهش حجم یافت که قابل توجه است.
برای استفاده از اینترنت سیستم عامل مادر در ms virtual pc می‌شود از منوی edit ، گزینه setting و انتخاب networking در صفحه ظاهر شده، تنظیم اولین adapter شبکه را بر روی shared networking NAT قرار داد و همه چیز به خوبی کار خواهد کرد. (البته برای استفاده از اینترنت در vmware باید روی کانکشن اینترنت خود در سیستم عامل مادر کلیک راست کرد و سپس انتخاب گزینه advanced و فعال سازی internet connection sharing بر روی کارت شبکه مجازی نصب شده آن ضروری خواهد بود)



نظرات مطالب
کار با Docker بر روی ویندوز - قسمت ششم - کار با بانک‌های اطلاعاتی درون Containerها
شما می‌توانید یک container مخصوص SQL Server داشته باشید و یکی هم مخصوص برنامه‌ی ASP.NET خودتان که IIS را اجرا می‌کند. بعد این وسط باید بین این‌ها سیم کشی کرد. این سیم کشی در قسمت بعدی تحت عنوان docker-compose بحث شده. همچنین چند مثال هم مانند «اجرای پروژه‌ی ASP.NET Core Music Store توسط docker-compose» در آن بحث شدند که به همراه این سیم‌کشی‌ها هستند.
مطالب
آموزش Cache در ASP.NET Core - (قسمت دوم : EasyCaching)
در قسمت اول، درمورد سیستم Cache پیش‌فرض موجود در Asp.Net Core و مزیت‌ها و معایب آن گفتیم. اگر قسمت اول را نخواندید، قسمت اول مقاله را میتوانید از این لینک بخوانید. 
 در این قسمت میخواهیم یک پکیج محبوب و کاربردی را برای پیاده سازی کش، در Asp.Net Core را بررسی کنیم.
در دنیای امروز، برنامه نویسی پکیج‌ها و فریمورک‌ها، نقش بسیار مهمی را ایفا میکنند؛ بطوریکه در بسیاری از این موارد، استفاده از این پکیج‌ها، عمل عاقلانه‌تری نسبت به دوباره نویسی فیچر‌های مربوطه است. برای عمل کشینگ در Asp.Net Core نیز پکیج‌های فوق‌العاده‌ای وجود دارند که در این مقاله، به بررسی و استفاده پکیج این میپردازیم.
در این پکیج، هر یک از متد‌های موجود در عملیات کشینگ، بصورت بهینه‌ای تعریف شده و قابل استفاده‌اند. سیستمی که این پکیج برای کش کردن داده‌ها استفاده میکند، همان سیستم کش Asp.Net Core هست و به‌نوعی، سوار بر این سیستم، قابلیت‌های بیشتر و بهتری را ارائه میدهد و این متد‌ها شامل موارد زیر هستند:
  1.  Get/GetAsync(with data retriever)
  2.  Get/GetAsync(without data retriever)
  3.  Set/SetAsync
  4.  Remove/RemoveAsync
  5.  ~~Refresh/RefreshAsync (was removed)~~
  6.  RemoveByPrefix/RemoveByPrefixAsync
  7.  SetAll/SetAllAsync
  8.  GetAll/GetAllAsync
  9.  GetByPrefix/GetByPrefixAsync
  10.  RemoveAll/RemoveAllAsync
  11.  GetCount
  12.  Flush/FlushAsync
  13.  TrySet/TrySetAsync
  14.  GetExpiration/GetExpirationAsync

مفهوم استفاده از این متد‌ها، با همان مفهوم متد‌های کش در core، برابری میکند که در قسمت اول این مقاله به آن پرداختیم. همانطور که می‌بینید، این پکیج از Async Method‌‌ها هم پشتیبانی میکند و میتوانید کش‌های خود را بصورت Async بنویسید.
یکی از قابلیت‌های دیگر این پکیج، سازگاری آن با انواع Cache Provider‌های موجود است. بطور خلاصه Cache Provider‌ها، همان ارائه دهندگان حافظه‌ی Ram، در قالب‌ها و ابزارهای مختلف هستند. برخی از این‌ها با داشتن الگوریتم‌های بهینه‌تر، سرعت بالاتری از رد و بدل کردن اطلاعات در Ram را در اختیار ما قرار میدهند و Local بودن یا Distributed بودن را کنترل میکنند. Cache provider‌های گوناگونی وجود دارند که هریک به شکلی کار میکند؛ برای مثال شما میتوانید با Provider ای مستقیما با خود Ram، برای Get و Set کردن کش‌های خود در ارتباط باشید و یا در روشی دیگر، از یک دیتابیس(Redis)، جدا از دیتابیس اصلی برنامه که حافظه مصرفی آن Ram هست و منابع حافظه شما را نیز مدیریت میکند، برای کش‌های خود استفاده کنید و اطلاعات را بصورت ایندکس گذاری شده در Ram ذخیره کنید که به سرعت واکشی آن می‌افزاید.

بطور کل Cache Provider هایی که پکیج EasyCaching با آن‌ها سازگار است شامل موارد زیر است:
  1. In-Memory
  2. Memcached
  3. Redis(Based on StackExchange.Redis)
  4. Redis(Based on csredis)
  5. SQLite
  6. Hybrid
  7. Disk
  8. LiteDb

یکی دیگر از مزیت‌های این پکیج، سازگاری آن با Serializer‌های مختلف است. همانطور که میدانید دیتا‌های ورودی و خروجی در برنامه، نیاز به Serialize شدن دارند. وقتی میخواهید دیتایی را در دیتابیس ذخیره کنید، آن را در قالب یک شی (Model) از کاربر دریافت میکنید و شما باید برای ذخیره این دیتا، اطلاعات درون شیء را به قالبی که قابل ذخیره شدن باشد، در آورید که این عمل Serialize نام دارد. دقیقا برعکس این روند، بعد از واکشی اطلاعات از دیتابیس، اطلاعات را در قالب اشیایی که قابل نمایش به کاربر باشد (DeSerialize) در میاوریم.
در کش کردن هم چیزی که شما با آن سروکار دارید، دیتا است؛ پس برای ذخیره و واکشی این دیتا، از هر حافظه‌ای، چه دیتابیس و چه Ram، باید از یک Serializer استفاده کنید تا عملیات Serialize و DeSerialize را برایتان انجام دهد. Serializer‌های مختلفی وجود دارند که بصورت پکیج‌هایی ارائه شده‌اند و اما Serializer هایی که سیستم EasyCaching آن‌هارا پشتیبانی میکند، شامل موارد ذیل هستند:
  1. BinaryFormatter
  2. MessagePack
  3. Newtonsoft.Json
  4. Protobuf
  5. System.Text.Json

در ادامه به پیاده سازی کش، با استفاده از EasyCaching در سه Provider مختلف از این پکیج می‌پردازیم.

 1_ پروایدر InMemory :
پروایدر InMemory، یک سیستم Local Caching را برای ما به وجود میاورد. در قسمت قبلی مقاله سیستم‌های Local(InMemory) و Distributed را بررسی کردیم و تفاوت‌های میان آن‌ها را گفتیم.

برای استفاده از پروایدر InMemory در EasyCaching باید پکیج زیر را نصب کنید: 
Install-Package EasyCaching.InMemory
در مرحله بعد، کانفیگ‌های مربوط به این پکیج را در کلاس Startup برنامه خود میاوریم. راحت‌ترین روش افزودن این پکیج به Startup، صرفا افزودن حالت پیشفرض آن به متد ConfigureServices است که به شرح زیر عمل میکنیم: 
  services.AddEasyCaching(options =>
 {
       // use memory cache with a simple way
        options.UseInMemory();
 }
این حالت از کانفیگ، پکیج تنظیمات پیش‌فرض خود پکیج را برای برنامه قرار میدهد؛ شما میتوانید با استفاده از option‌های دیگری که در متد ()UseInMemory وجود دارند، تنظیمات شخصی سازی شده از سیستم کشینگ خود را اعمال کنید. 
و تمام. هم اکنون میتوان با استفاده از اینترفیس IEasyCachingProvider که این سرویس در اختیارمان قرار داده و عمل تزریق وابستگی آن در کلاس‌ها و کنترلر‌های مان دیتای در حال عبور را کش کنیم. متد‌های موجود در این اینترفیس به شرح زیر میباشد : 
// تنظیم یک کش با کلید - مقدار - زمان انقضا
void Set<T>(string cacheKey, T cacheValue, TimeSpan expiration);
Task SetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration);

// تنظیم یک کش با مقدار و زمان انقضا که تایپ مقدار از نوع دیکشنری هست و کلید دیکشنری بعنوان کلید کش قرار میگیرد
void SetAll<T>(IDictionary<string, T> value, TimeSpan expiration);
Task SetAllAsync<T>(IDictionary<string, T> value, TimeSpan expiration);

// تنظیم یک کش با کلید - مقدار - زمان انقضا
// اگر کلیدی همنام وجود داشته باشد مقدار نادرست و در غیر اینصورت مقدار نادرست را برمیگرداند
bool TrySet<T>(string cacheKey, T cacheValue, TimeSpan expiration);
Task<bool> TrySetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration);
 
// گرفتن یک کش با کلید
CacheValue<T> Get<T>(string cacheKey);
Task<CacheValue<T>> GetAsync<T>(string cacheKey);

// 
CacheValue<T> Get<T>(string cacheKey, Func<T> dataRetriever, TimeSpan expiration);
Task<CacheValue<T>> GetAsync<T>(string cacheKey, Func<Task<T>> dataRetriever, TimeSpan expiration);
 
// گرفتن یک کش با چند کاراکتر پیشین کلید آن
// برای مثال یک کلید با نام
// MyKey
// تنها با داشتن چند حرف اول 
// MyK
// میتوانیم این کش را دریافت کنیم
IDictionary<string, CacheValue<T>> GetByPrefix<T>(string prefix);
Task<IDictionary<string, CacheValue<T>>> GetByPrefixAsync<T>(string prefix);

// 
IDictionary<string, CacheValue<T>> GetAll<T>(IEnumerable<string> cacheKeys);
Task<IDictionary<string, CacheValue<T>>> GetAllAsync<T>(IEnumerable<string> cacheKeys);

// گرفتن تعداد کش‌های با کاراکتر‌های پیشین کلید که میان چند کلید یکسان است 
int GetCount(string prefix = "");
Task<int> GetCountAsync(string prefix = "");

// گرفتن زمان انقضا باقیمانده از یک کش با کلید آن
TimeSpan GetExpiration(string cacheKey);
Task<TimeSpan> GetExpirationAsync(string cacheKey);

// حذف کردن یک کش با کلید
void Remove(string cacheKey);
Task RemoveAsync(string cacheKey);

// حذف کردن یک کش با چند کاراکتر پیشین کلید
void RemoveByPrefix(string prefix);
Task RemoveByPrefixAsync(string prefix);
 
// حذف کردن چند کش با لیستی از کلید‌ها void RemoveAll(IEnumerable<string> cacheKeys);
Task RemoveAllAsync(IEnumerable<string> cacheKeys);

// بررسی وجود یا عدم وجود یک کش با کلید
bool Exists(string cacheKey);
Task<bool> ExistsAsync(string cacheKey);

// حذف کردن همه کش‌ها void Flush();
Task FlushAsync();

همانطور که قبلا گفته شد، سیستم کش، با دیتا مرتبط است و نیازمند یک Object Serializer جهت Serialize کردن اطلاعات ورودی و ذخیره آن در Target Storage مشخص شده است. پکیج EasyCaching برای Provider‌های خود، یک Object Serializer پیش‌فرض قرار داده‌است و تا وقتی که شما آن را طبق نیازی خاص، بصورت سفارشی تغییر نداده باشید، از آن استفاده میکند.
در میان پنج Serializer معرفی شده که EasyCaching آن‌ها را پشتیبانی میکند، BinaryFormatter بصورت پیش‌فرض در همه‌ی Provider‌ها برقرار است و تا وقتی یک Serializer انتخابی به EasyCaching معرفی نکنید، این پکیج از این Serializer استفاده میکند.
برای استفاده از Serializer‌های دیگری که معرفی شده میتوانید از لینک‌های زیر کمک بگیرید :

2 - پروایدر Redis :
ردیس، یک دیتابیس Key Value محور هست که محل ذخیره سازی آن Ram است و اطلاعات، بصورت موقت در آن ذخیره میشوند. بطور خلاصه، Key Value یعنی یکبار کلید و مقداری برای آن کلید تعریف میشود و هر وقت نام کلید تعریف شده، صدا زده شد، مقدار نسبت داده شده به آن، در اختیار ما قرار میگیرد. برای مثال کلید "Name" و مقدار "James". با این انتساب، هروقت "Name" فراخوانده شود، مقدار "James" را خواهیم داشت. سیستم Key Value بخاطر عدم پیچیدگی و سادگی‌ای که دارد، بسیار سریع عمل میکند و همچنین ایندکس گذاری‌هایی که ردیس روی دیتا‌ها انجام میدهد، باعث افزایش سرعت آن نیز خواهد شد که ردیس را به سریع‌ترین دیتابیس Key Value دنیا تبدیل کرده.
در اینجا با توجه به قابلیت هایی که ردیس داراست، یکی از بهترین گزینه‌ها برای انتخاب بعنوان فضای ذخیره سازی کش‌ها بصورت Distributed است.
برای استفاده از این دیتابیس قدرتمند ابتدا باید از طریق یکی از روش‌های معمول اقدام به نصب آن کنید. میتوانید فایل نصبی را از وبسایت رسمی آن دانلود کنید و یا یا با استفاده از Docker اقدام به نصب آن نمایید.
پس از نصب این دیتابیس روی سیستم خود ، برای استفاده از آن در EasyCaching ابتدا باید پکیج مورد نیاز را نصب کنید. 
Install-Package EasyCaching.Redis
ادامه کار به همان سادگی پروایدر قبلی هست و فقط کافیست EasyCaching و option ردیس را به کلاس Startup اضافه کنید. 
 services.AddEasyCaching(option =>
{
       option.UseRedis(config =>
      {
             config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
      });
});
با استفاده از متد UseRedis شما قابلیت استفاده از ردیس را در EasyCaching فعال میکنید و سپس باید اطلاعات Host و Port ردیس نصب شده‌ی روی سیستم خود را به این متد معرفی کنید.
اگر ردیس را بدون تنظیمات شخصی سازی شده و در همان حالت پیش‌فرض خودش نصب کرده باشید، Host و Port شما مانند نمونه بالا 127.0.0.1 و 6379 خواهد بود و نیازی به تغییر نیست.
در مرحله بعد برای استفاده از پروایدر ردیس ، اینترفیس IRedisCachingProvider در سرتاسر برنامه در دسترس خواهد بود. این اینترفیس علاوه بر اینکه متد‌های اصلی موجود در EasyCaching را ساپورت کرده ، بخاطر ساختار دیتابیسی که خود ردیس در اختیار ما قرار میدهد قابلیت‌های بیشتری نیز اراعه خواهد داد. این قابلیت‌ها خصیصه‌های ردیس هست چرا که این دیتابیس هم دقیقا شبیه به ساختار سیستم کش Key , Value را پشتیبانی میکند و در پی آن قابلیت هایی برای مدیریت بهتر کلید‌ها و مقادیر اراعه میدهد.
اینترفیس IRedisCachingProvider شامل تعداد زیادی از متد‌ها برای پشتیبانی از قابلیت‌های ردیس است که در ادامه همه آنهارا نام برده و برخی را توضیح مختصری خواهیم داد:
  • متد‌های Keys 
// حذف کردن یک کلید در صورت وجود
bool KeyDel(string cacheKey);
Task<bool> KeyDelAsync(string cacheKey);

// تنظیم تاریخ انتضا به یک کلید موجود بر حسب ثانیه
bool KeyExpire(string cacheKey, int second);
Task<bool> KeyExpireAsync(string cacheKey, int second);

// بررسی وجود یا عدم وجود یک کلید
bool KeyExists(string cacheKey);
Task<bool> KeyExistsAsync(string cacheKey);

// گرفتن زمان انتقضا باقیمانده یک کلید
long TTL(string cacheKey);
Task<long> TTLAsync(string cacheKey);

// جستجو بین همه کلید‌ها براساس فیلتر شامل بودن نام کلید از مقدار ورودی
List<string> SearchKeys(string cacheKey, int? count = null);
  • متد‌های String 
// افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید
long IncrBy(string cacheKey, long value = 1);
Task<long> IncrByAsync(string cacheKey, long value = 1);

// افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید
double IncrByFloat(string cacheKey, double value = 1);
Task<double> IncrByFloatAsync(string cacheKey, double value = 1);

// تنظیم یک کلید و مقدار وقتی مقدار از نوع رشته باشد
bool StringSet(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = "");
Task<bool> StringSetAsync(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = "");

// گرفتن کلید و مقدار آن وقتی مقدار از نوع رشته باشد
string StringGet(string cacheKey);
Task<string> StringGetAsync(string cacheKey);

// گرفتن تعداد کاراکتر‌های مقدار یک کلید وقتی مقدار از نوع رشته باشد
long StringLen(string cacheKey);
Task<long> StringLenAsync(string cacheKey);

// جایگزاری یک رشته درون رشته مقدار یک کلید بعد از شماره کاراکتر مشخص شده در ورودی برای مثال 
// "Hello World"
// 6 , jack
// "Hello jack"
long StringSetRange(string cacheKey, long offest, string value);
Task<long> StringSetRangeAsync(string cacheKey, long offest, string value);

// گرفتن یک بازه از رشته مقدار یک کلید با شماره کاراکتر شروع و پایان
string StringGetRange(string cacheKey, long start, long end);
Task<string> StringGetRangeAsync(string cacheKey, long start, long end);
  • متد‌های Hashes
// شما میتوانید دو کلید با نام‌های یکسان داشته باشید که در کلید تایپ دیکشنری مقدار خود باهم متفاوت هستند
bool HMSet(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null);
Task<bool> HMSetAsync(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null);

// شما میتوانید دو کلید با نام‌های یکسان داشته باشید که در ورودی فیلد باهم متفاوت هستند
bool HSet(string cacheKey, string field, string cacheValue);
Task<bool> HSetAsync(string cacheKey, string field, string cacheValue);

// بررسی وجود یا عدم وجود یک کلید و فیلد
bool HExists(string cacheKey, string field);
Task<bool> HExistsAsync(string cacheKey, string field);

// حذف کردن کلید‌های همنام موجود با همه فیلد‌های متفاوت در حالت پیشفرض مگر اینکه کلید و نام فیلد را بهمراه آن مشخص کنید
long HDel(string cacheKey, IList<string> fields = null);
Task<long> HDelAsync(string cacheKey, IList<string> fields = null);

// گرفتن مقدار با نام کلید و نام فیلد
string HGet(string cacheKey, string field);
Task<string> HGetAsync(string cacheKey, string field);

// گرفتن فیلد و مقدار با کلید
Dictionary<string, string> HGetAll(string cacheKey);
Task<Dictionary<string, string>> HGetAllAsync(string cacheKey);

//  افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید و فیلد
long HIncrBy(string cacheKey, string field, long val = 1);
Task<long> HIncrByAsync(string cacheKey, string field, long val = 1);

// گرفتن فیلد‌های متفاوت یک کلید
List<string> HKeys(string cacheKey);
Task<List<string>> HKeysAsync(string cacheKey);

// گرفتن تعداد فیلد‌های متفاوت یک کلید
long HLen(string cacheKey);
Task<long> HLenAsync(string cacheKey);

// گرفتن مقادیر یک کلید بدون در نظر گرفتن فیلد‌های متفاوت
List<string> HVals(string cacheKey);
Task<List<string>> HValsAsync(string cacheKey);

// گرفتن مقدار دیکشنری با کلید و نام فیلد‌ها Dictionary<string, string> HMGet(string cacheKey, IList<string> fields);
Task<Dictionary<string, string>> HMGetAsync(string cacheKey, IList<string> fields);
  • متد‌های List
// گرفتن یک مقدار از لیست مقادیر با شماره ایندکس آن
T LIndex<T>(string cacheKey, long index);
Task<T> LIndexAsync<T>(string cacheKey, long index);

// گرفتن تعداد مقادیر در لیست یک کلید
long LLen(string cacheKey);
Task<long> LLenAsync(string cacheKey);

// گرفتن اولین مقدار از مقادیر یک لیست در یک کلید
T LPop<T>(string cacheKey);
Task<T> LPopAsync<T>(string cacheKey);

// ایجاد یک کلید که لیستی از مقادیر را پشتیبانی میکند و میتوانید هر بار مقدار جدید به لیست آن اضافه کنید
long LPush<T>(string cacheKey, IList<T> cacheValues);
Task<long> LPushAsync<T>(string cacheKey, IList<T> cacheValues);

// گرفتن مقادیر یک لیست از داده بر اساس شماره ایندکس شروع و پایان برای مثال مقادیر ۳ تا ۷ از ۱۰ مقدار
List<T> LRange<T>(string cacheKey, long start, long stop);
Task<List<T>> LRangeAsync<T>(string cacheKey, long start, long stop);

// حذف کردن مقادیر یک لیست بر اساس تعداد وارد شده که بعد از مقدار وارد شده شروع به شمارش میشود
long LRem<T>(string cacheKey, long count, T cacheValue);
Task<long> LRemAsync<T>(string cacheKey, long count, T cacheValue);

// افزودن یک مقدار به لیستی از مقادیر یک کلید با گرفتن شماره ایندکس
bool LSet<T>(string cacheKey, long index, T cacheValue);
Task<bool> LSetAsync<T>(string cacheKey, long index, T cacheValue);

// بررسی میکند که لیست مقداری برای شماره ایندکس شروع و پایان درون خودش دارد یا خیر
bool LTrim(string cacheKey, long start, long stop);
Task<bool> LTrimAsync(string cacheKey, long start, long stop);

//  https://redis.io/commands/lpushx
long LPushX<T>(string cacheKey, T cacheValue);
Task<long> LPushXAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/linsert
long LInsertBefore<T>(string cacheKey, T pivot, T cacheValue);
Task<long> LInsertBeforeAsync<T>(string cacheKey, T pivot, T cacheValue);

// https://redis.io/commands/linsert
long LInsertAfter<T>(string cacheKey, T pivot, T cacheValue);
Task<long> LInsertAfterAsync<T>(string cacheKey, T pivot, T cacheValue);

// https://redis.io/commands/rpushx
long RPushX<T>(string cacheKey, T cacheValue);
Task<long> RPushXAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/rpush
long RPush<T>(string cacheKey, IList<T> cacheValues);
Task<long> RPushAsync<T>(string cacheKey, IList<T> cacheValues);

// https://redis.io/commands/rpop
T RPop<T>(string cacheKey);
Task<T> RPopAsync<T>(string cacheKey);
  • متد‌های Set
// https://redis.io/commands/SAdd
long SAdd<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null);
Task<long> SAddAsync<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null);
       
// https://redis.io/commands/SCard
long SCard(string cacheKey);
Task<long> SCardAsync(string cacheKey);

// https://redis.io/commands/SIsMember
bool SIsMember<T>(string cacheKey, T cacheValue);
Task<bool> SIsMemberAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/SMembers
List<T> SMembers<T>(string cacheKey);
Task<List<T>> SMembersAsync<T>(string cacheKey);

// https://redis.io/commands/SPop
T SPop<T>(string cacheKey);
Task<T> SPopAsync<T>(string cacheKey);

// https://redis.io/commands/SRandMember
List<T> SRandMember<T>(string cacheKey, int count = 1);
Task<List<T>> SRandMemberAsync<T>(string cacheKey, int count = 1);

// https://redis.io/commands/SRem
long SRem<T>(string cacheKey, IList<T> cacheValues = null);
Task<long> SRemAsync<T>(string cacheKey, IList<T> cacheValues = null);
  • متد‌های Stored Set
// https://redis.io/commands/ZAdd
long ZAdd<T>(string cacheKey, Dictionary<T, double> cacheValues);
Task<long> ZAddAsync<T>(string cacheKey, Dictionary<T, double> cacheValues);
       
// https://redis.io/commands/ZCard       
long ZCard(string cacheKey);
Task<long> ZCardAsync(string cacheKey);

// https://redis.io/commands/ZCount
long ZCount(string cacheKey, double min, double max);
Task<long> ZCountAsync(string cacheKey, double min, double max);

// https://redis.io/commands/ZIncrBy
double ZIncrBy(string cacheKey, string field, double val = 1);
Task<double> ZIncrByAsync(string cacheKey, string field, double val = 1);

// https://redis.io/commands/ZLexCount
long ZLexCount(string cacheKey, string min, string max);
Task<long> ZLexCountAsync(string cacheKey, string min, string max);

// https://redis.io/commands/ZRange
List<T> ZRange<T>(string cacheKey, long start, long stop);
Task<List<T>> ZRangeAsync<T>(string cacheKey, long start, long stop);

// https://redis.io/commands/ZRank
long? ZRank<T>(string cacheKey, T cacheValue);
Task<long?> ZRankAsync<T>(string cacheKey, T cacheValue);

// https://redis.io/commands/ZRem
long ZRem<T>(string cacheKey, IList<T> cacheValues);
Task<long> ZRemAsync<T>(string cacheKey, IList<T> cacheValues);

// https://redis.io/commands/ZScore
double? ZScore<T>(string cacheKey, T cacheValue);
Task<double?> ZScoreAsync<T>(string cacheKey, T cacheValue);
  • متد‌های Hyperloglog
// https://redis.io/commands/PfAdd
bool PfAdd<T>(string cacheKey, List<T> values);
Task<bool> PfAddAsync<T>(string cacheKey, List<T> values);

// https://redis.io/commands/PfCount
long PfCount(List<string> cacheKeys);
Task<long> PfCountAsync(List<string> cacheKeys);

// https://redis.io/commands/PfMerge
bool PfMerge(string destKey, List<string> sourceKeys);
Task<bool> PfMergeAsync(string destKey, List<string> sourceKeys);
  • متد‌های Geo
// https://redis.io/commands/GeoAdd
long GeoAdd(string cacheKey, List<(double longitude, double latitude, string member)> values);
Task<long> GeoAddAsync(string cacheKey, List<(double longitude, double latitude, string member)> values);

// https://redis.io/commands/GeoDist
double? GeoDist(string cacheKey, string member1, string member2, string unit = "m");
Task<double?> GeoDistAsync(string cacheKey, string member1, string member2, string unit = "m");

// https://redis.io/commands/GeoHash
List<string> GeoHash(string cacheKey, List<string> members);
Task<List<string>> GeoHashAsync(string cacheKey, List<string> members);

// https://redis.io/commands/GeoPos
List<(decimal longitude, decimal latitude)?> GeoPos(string cacheKey, List<string> members);
Task<List<(decimal longitude, decimal latitude)?>> GeoPosAsync(string cacheKey, List<string> members);
برای اطلاعات بیشتر از متد‌های دیگر موجود در ردیس میتوانید از این لینک استفاده کنید. 

3 - پروایدر Hybrid :
این پروایدر، روشی از کشینگ را مابین local caching و distributed caching، ارائه میدهد و میتوانید از یک پروایدر Local مثل InMemory و پروایدر Distributed مثل Redis، همزمان باهم استفاده کنید که در یک کانال باهم و در راستای هم کار میکنند.
اما سوال اینجاست که این قابلیت دقیقا چه کاری انجام میدهد؟
همانطور که قبلا گفته شد، کش In-Memory سرعت بالاتری نسبت به کش Distributed دارد؛ اما دچار معایبی در حالت چند سروری هست که این معایب از جمله حذف شدن دیتای یک سرور، در صورت Down شدن آن، Sync نبودن کش سرور‌ها باهم دیگر و دو نسخه، کش کردن دیتا در هر سرور و موارد دیگری که میتوان نام برد. اما از طرفی کش Distributed مشکلات چند سروری را با قرار دادن یک مرکزیت واحد کش در حافظه شبکه شده سرور‌ها برطرف میکند و اطلاعات سرور‌ها، از یک منبع خوانده میشود و طبعا مشکلات In-Memory را نخواهیم داشت؛ اما به دلیل رد و بدل شدن دیتا در محیط شبکه و عمل Serialize , Deserialize که هنگام عبور دیتا روی آن صورت میگیرد، بخشی از سرعت، کاهش خواهد یافت و درنهایت Performance کمتری را نسبت به In-Memory ارائه میدهد.
حالا برای اینکه بتوانیم سیستم کش خودمان را طوری طراحی کنیم که عیب‌های (Local)In-Memory و Distributed را نداشته باشیم و بتوانیم از هریک به شکلی درست استفاده کنیم که هم اطلاعاتمان Sync باشد و هم از سرعت بالای In-Memory برخوردار شویم، میتوانیم از پروایدر Hybrid استفاده کنیم. 

شیوه کار این پروایدر به این صورت است که وقتی برنامه برای بار اول به کش In-Memory درخواستی را ارسال میکند و کش مورد نظر در آن وجود ندارد، برنامه یک درخواست دیگر را به کش Distributed ارسال میکند و دیتای مورد نظر را به کاربر بازگشت میدهد و علاوه بر آن یک کپی از کش آن دیتا، در کش In-Memory هم ایجاد میکند. با این ساختار از دفعات بعد که کاربر درخواستی را ارسال کند، دیتای درخواستی در In-Memory نیز موجود خواهد بود و سریع‌تر از بار اول پاسخ را ارسال خواهد کرد.
از طرفی نیز وقتی کاربر دیتای جدیدی را ذخیره میکند، ابتدا آن دیتا در In-Memory کش شده و سپس با درخواست خود پروایدر، در کش Distributed هم اعمال میشود تا در نهایت دیتابیس نیز آن را ذخیره کند.
وقتی این اتفاق می‌افتد، پروایدر Hybrid با کمک پکیج Bus.Redis به کش In-Memory سرور‌های دیگر دستور Pull کردن دیتا کش‌های جدید را ارسال میکند و در نهایت همه سرور‌ها نیز به کمک Distributed مرکزی باهم Sync خواهند بود.

برای فعال سازی این پروایدر باید پکیج‌های زیر را در برنامه خود نصب کنید: 
Install-Package EasyCaching.HybridCache
Install-Package EasyCaching.InMemory
Install-Package EasyCaching.Redis
Install-Package EasyCaching.Bus.Redis
در این مجموعه از پکیج‌ها، از یک پروایدر Local(InMemory) و یک پروایدر distributed(Redis) استفاده شده و همانطور که گفته شد، مدیریت هماهنگ سازی این دو، توسط پکیج دیگری بنام EasyCaching.Bus.Redis صورت میگیرد.

تنظیمات فعالسازی این پروایدر هم متشکل از تنظیمات دو پروایدر In-Memory و Redis، بعلاوه معرفی این دو به هم در متد UseHybrid خواهد بود. 
   services.AddEasyCaching(option =>
       // local
       option.UseInMemory("c1");

       // distributed
       option.UseRedis(config =>
                config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
       }, "c2");

       // combine local and distributed
        option.UseHybrid(config =>
                 // specify the local cache provider name after v0.5.4
                   config.LocalCacheProviderName = "c1"
                // specify the distributed cache provider name after v0.5.4
                   config.DistributedCacheProviderName = "c2"
        });

          // use redis bus
           .WithRedisBus(busConf =>
                   busConf.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
           });
});
برای استفاده از این پروایدر، متفاوت با پروایدر‌های قبلی، باید اینترفیس IHybridCachingProvider را فراخوانی کنیم. متد‌های موجود در این اینترفیس، همان متدهایی است که در اینترفیس IEasyCachingProvider وجود دارند و از نظر نام متد و روش استفاده، تفاوتی میان آن نیست.

 پیشنهاد شخصی در Distributed Cache‌ها 
همانطور که گفته شد Distributed کش‌ها، گزینه مناسب‌تری برای برنامه‌های چند سروری هستند؛ اما در این حالت مواردی مثل Round Trip شبکه و جابجایی اطلاعات در این محیط بعلاوه Serialize , Deserialize هایی که باید انجام شوند دلیلی میشود تا سرعت آن در پاسخ به درخواست‌های برنامه، نسبت به حالت تک سروری(In-Memory) کمتر باشد. Hybrid Provider یکی از روش‌های حل این مشکل بوده که معرفی کردیم. اما برای اینکه تیر خلاص را به پیکره سیستم Distributed Cache خود بزنید و تریک فنی آخر را نیز روی آن اجرا کنید، پیشنهاد میکنم از پکیج EasyCaching.Extensions.EasyCompressor که بر پایه پکیج EasyCaching نوشته شده استفاده کنید. این پکیج، اطلاعات را قبل از کش شدن، فشرده سازی میکند و حجم اطلاعات را به طور محسوسی کاهش میدهد که میزان فضای اشغالی Ram را کم کرده و همچنین عمل جابجایی اطلاعات را نیز تسریع می‌بخشد. میتوانید از این پکیج هم در Redis و هم در Hybrid استفاده کنید. چگونگی استفاده از آن نیز در لینک Github ذکر شده موجود است.

معرفی پروژه
تا اینجا با مفاهیمی که برای شروع استفاده حرفه‌ای از کش در پروژه‌تان نیاز بود، آشنا شدید. در پروژه‌های واقعی، میتوانیم از این سیستم به روش‌های مختلفی در سطوح مختلفی از برنامه استفاده کنیم؛ برای مثال کد‌های مربوط به عملیات کش را میتوان بصورت ساده‌ای در هر کنترلر تزریق و در اکشن‌ها استفاده کرد؛ یا از لایه کنترلر، آن را به لایه سرویس منتقل کرد. در روشی دیگر میتوانیم یک Attribute را برای این عمل در نظر بگیریم و یا اینکه آن را بصورت یک Middleware اختصاصی در برنامه پیاده کنیم. 
در این پروژه علاوه بر اینکه سعی کرده‌ام استفاده از Provider‌های معرفی شده را در محیط واقعی‌تر پیاده سازی کنم، در هر پروژه از این Solution، کش را به شیوه‌ای متفاوت در لایه‌های مختلفی از برنامه قرار داده‌ام تا شما همراهان بتوانید طبق نیازتان از روشی مناسب و بهینه در پروژه‌های واقعی خود از آن استفاده کنید.
مطالب
قسمت دوم -- نگاهی دقیق تر به اولین پروژه VC++ (درک مفهوم فایلهای سرآیند و فضای نام ، ویژگیهای زبان ++C و برخی قوانین برنامه نویسی در ++C)
در این قسمت نگاهی دقیق‌تر به فایلهای سرآیند ، فضای نام ، ویژگیهای زبان ++C  و برخی قوانین برنامه نویسی ++C  خواهیم داشت و همچنین در مورد  اولین پروژه توضیحات جامع‌تری ارائه میکنیم .

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

زبانهای ماشین :
  زبانی که مستقیما و بدون نیاز به ترجمه قابل فهم توسط کامپیوتر می‌باشد . هر پردازنده یا processor  زبان خاص خود را دارد !... در نتیجه تنوع زبان ماشین بستگی به انواع پردازنده‌های موجود دارد و اگر دو کامپیوتر دارای پردازنده‌های یکسان نباشتد ، زبان ماشین آنها با یکدیگر متفاوت و غیر قابل فهم برای دیگری می‌باشد . زبان ماشین وابسته به ماشین یا Machine independent  می‌باشد . تمامی دستورات در این زبان توالی از 0 و 1 می‌باشند . برنامه‌های اولیه را با این زبان می‌نوشتند در نتیجه نوشتن برنامه سخت و احتمال خطا داشتن در آن زیاد بود . ار آنجا که نوشتن برنامه به این زبان سخت و فهم برنامه‌های نوشته شده به آن دشوار بود ، برنامه نویسان به فکر استفاده از حروف بجای دستورات زبان ماشین افتادند ( پیدایش زبان اسمبلی )

زبانهای اسمبلی :
به زبانی که دستورات زبان ماشین را با نمادهای حرفی بیان میکند، زبان اسمبلی  (Assembley Language) می‌گویند . چون این زبان مستقیما قابل فهم برای کامپیوتر نیست باید قبل از اجرا آن را به زبان ماشین ترجمه کرد ، به این مترجم اسمبلر گفته میشود . برنامه‌های نوشته شده به این زبان قابل فهم برای برنامه نویس بود اما از آنجا که به ازای هر دستور زبان ماشین یک دستور زبان اسمبلی داشتیم از حجم برنامه‌ها کاسته نشد ! .. بعلاوه چون زبان اسمبلی همانند زبان ماشین از دستورات پایه ای و سطح پایین استفاده میکرد نوشتن برنامه با این زبان هم سخت و مشکل بود . لذا اهل خرد به فکر ابداع نسلی از زبانهای بهتر بودند (پیدایش زبانهای سطح بالا)

زبانهای سطح بالا :
زبانهای سطح بالا قابل فهم بودند و این امکان را داشتند تا چند دستور زبان ماشین یا اسمبلی را بتوان در قالب یک دستور نوشت ( منظور توابع کتابخانه ای در ++C/C) . پس هم فهم ، هم نوشتن برنامه در این زبانها راحت و هم تعداد خطوط کد کمتر شد . این زبانها به زبانهای برنامه نویسی سطح بالا یا High-Level Programming Language  معروفند . البته برنامه نوشته شده در این زبان نیز برای کامپیوتر قابل فهم نبوده و باید به زبان ماشین ترجمه شوند ، این وظیفه بر عهده کامپایلر می‌باشد . اولین زبانهای برنامه نویسی سطح بالا مانند  FORTRAN ، COBOL ، PASCAL و C  می‌باشند . زبان برنامه نویسی ++C تکامل یافته زبان  میباشد .
هر یک از زبانهای برنامه نویسی سطح بالا یک روش برنامه نویسی را پشتیبانی میکند به طور مثال زبان C  و  PASCAL  از روشهای برنامه نویسی  ساخت یافته ای و پیمانه ای و زبانهای مانند ++C و JAVA  از روش برنامه سازی شی گرا یا Object Oriented Programming یا به اختصار (OOP) استفاده میکنند . زبان ++C  چون زبان C را بطور کامل در بر  دارد پس از هر سه روش برنامه نویسی ساخت یافته و پیمانه ای و شی گرا استفاده میکند .

تا اینجا با تاریخچه ای از زبانها و مراحل تکامل آنها آشنا شدیم . حال ویژگیها و دلایل استفاده از زبان  ++C  را مرور میکنیم :

زبان C  در سال 1972 توسط دنیس ریچی طراحی شد . زبان C تکامل یافته زبان  BCPL است که طراح آن مارتین ریچاردز می‌باشد ، زبان BCPL نیز از زبان B  مشتق شده است که طراح آن  کن تامسون بود . (خداوند روح دنیس ریچی را همچون هوگو چاوز با مسیح بازگرداند ! ...) .
از این زبان برای نوشتن برنامه‌های سیستمی ، همچون سیستم عامل ، کامپایلر ، مفسر ، ویرایشگر ، برنامه‌های مدیریت بانک اطلاعاتی ، اسمبلر استفاده میکنند .
زبان C  برای اجرای بسیاری از دستوراتش از توابع کتابخانه ای استفاده میکند و بیشتر خصوصیات وابسته به سخت افزار را به این توابع واگذار میکند لذا نرم افزار تولید شده با این زبان به سخت افزار خاصی بستگی ندارد و با اندکی تغییرات می‌توانیم نرم افزار مورد نظر را روی ماشینهای متفاوت اجرا کنیم ، در نتیجه برنامه نوشته شده با C  قابلیت انتقال (Portability) دارند . بعلاوه کاربر میتواند توابع کتابخانه ای خاص خودش را بنویسد و از آنها در برنامه هایش استفاده کند .
برنامه‌های مقصدی که توسط کامپیلرهای C  ساخته میشود بسیار فشرده و کم حجم‌تر از برنامه‌های مشابه در سایر زبانهاست ، این امر باعث افزایش سرعت اجرای آنها میشود .
++C  که از نسل  C  است تمام ویژگیهای ذکر شده بالا را دارد ، علاوه بر آن شی گرا نیز می‌باشد . برنامه‌های شی گرا منظم و ساخت یافته اند و قابل آپدیت هستند و به سهولت تغییر و بهبود می‌یابند و قابلیت اطمینان و پایداری بیشتری دارند .

تحلیل  اولین پروژه :



در اولین پروژه  کد فوق را بکار بردیم ، حال به شرح دستورات آن می‌پردازیم .
#include <iostream>
دستوراتی که علامت # پیش از آنها قرار میگیرد ، دستورات راهنمای پیش پردازنده هستند . این خط یک دستور پیش پردازنده است که توسط پیش پردازنده و قبل از شروع کامپایل ، پردازش میشود . این کد فایل iostream  را به برنامه اضافه میکند . کتابخانه استاندارد  ++C  به چندین بخش تقسیم شده است و هر بخش فایل سرآیند خود را دارد . دلیل قرار گرفتن این دستور در ابتدای برنامه این است  که ، پیش از استفاده از هر تابع و فراخوانی کردن آن در برنامه ، کامپایلر لازم است اطلاعاتی در مورد آن تابع داشته باشد .  در خط کد بالا فایل سرآیند  iostream  استفاده نمودیم زیرا شامل توابع مربوط به I/O  ( ورودی / خروجی ) می‌باشد .

int  main()
{
 
   return 0;
}
دستور فوق بخشی از هر برنامه ++C  است ، main  تابع اصلی هر برنامه ++C است که شروع برنامه از آنجا آغاز می‌شود . کلمه  int  در ابتدای این خط ، مشخص میکند که تابع main پس از اجرا و به عنوان مقدار برگشتی (;return 0)  یک عدد صحیح باز می‌گرداند .

std::cout<<"Hello world ...\n";
دستور فوق یک رشته را در خروجی استاندارد که معمولا صفحه نمایش می‌باشد ارسال میکند . std  یک فضای نام است . فضای نام محدوده ای است که چند موجودیت در آن تعریف شده است . مثلا موجودیت cout  در فضای نام std در فایل سرآیند iostream تعریف شده است .


در زبان ++C  هر دستور به ; (سیموکالن) ختم میشود .


 

مطالب
ذخیره و بازیابی فایل در Mongodb (بخش سوم)
در قسمت‌های پیشین (^ ،^ ) در مورد عملیات CRUD در سطح دیتابیس و به طور کلی در مورد ایندکس گذاری صحبت کردیم. در این بخش قصد داریم یکی از موارد بسیار مهم، یعنی ذخیره‌ی فایل‌های باینری را در دیتابیس، مورد بررسی قرار دهیم. روش‌های مختلفی برای اینکار وجود دارند؛ ولی بعضی از این روش‌ها در حال حاضر منسوخ شده اعلام شده‌اند که در اینجا ما آخرین روش را که در حال حاضر هیچ ویژگی منسوخ شده‌ای ندارد، به کار می‌گیریم.
از آنجاکه نهایت اندازه‌ی یک سند BSON نمی‌تواند بیشتر از 16 مگابایت باشد، قابلیتی به نام GridFS ایجاد شده‌است تا بتوان فایل‌های باینری را در آن ذخیره کرد. GridFS شامل دو بخش مختلف برای ذخیره اطلاعات یک فایل باینری است:
- fs.chunks که برای ذخیره اطلاعات قطعه‌های یک فایل باینری به کار میرود.
- fs.files که برای ذخیره اطلاعات و متادیتاها به کار می‌رود.

قبل از هر چیزی باید بدانید که کتابخانه مربوط به GridFs در یک پکیج جداگانه عرضه شده است و باید آن از طریق nuget نصب کنیم:
install-package MongoDB.Driver.GridFS

برای آپلود یک فایل باینری به داخل سیستم از کد زیر استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
ابتدا یک شیء GridFsBucket را ایجاد میکنیم که از ما اطلاعات دیتابیس مورد نظر را برای ارسال فایل میخواهد و نتیجه‌ی آن یک کلاس از جنس اینترفیس IGridFSBucket می‌باشد. این باکت یا سطل در واقع همانند کالکشن رفتار میکند.
byte[] source=File.ReadAllBytes(@"D:\Untitled.png");
var options = new GridFSUploadOptions
            {
                ChunkSizeBytes = 64512, // 63KB
                Metadata = new BsonDocument
                {
                    { "CoverType", "Front" }, 
                    { "copyrighted", true }
                }
            };
در مرحله بعد فایل باینری را به صورت آرایه‌ای از بایت‌ها میخوانیم (البته حالت‌های مختلفی چون استریم را نیز پشتیبانی میکند). بعد از خواندن فایل، یک شیء از جنس کلاس GridFSUploadOptions را ایجاد و اطلاعات فایل آپلودی را مشخص می‌کنیم. به عنوان مثال اولین خصوصیتی که پر میکنیم خصوصیت تعیین سایز قطعات فایل باینری می‌باشد که به طور پیشفرض بر روی 64 مگابایت قرار گرفته است و عموما هم برای اکثر موارد، پاسخگوی نیاز‌ها است (در بخش بعدی مقالات، بیشتر این مورد را بررسی میکنیم).
مورد دومی که مقداردهی شده‌است، متادیتا‌ها هستند و این قابلیت را داریم که پرس و جوی خود را بر اساس آن‌ها نیز فیلتر کنیم. این خصوصیت مقدار دریافتی از جنس BsonDocument را دریافت میکند. ولی اگر شما برای فایل خود، کلاس اختصاصی برای متادیتاها در نظر گرفته‌اید میتوانید از یک Extension Method به نام ToBsonDocument استفاده کنید و شیء خود را به این نوع تبدیل کنید:
 var options = new GridFSUploadOptions
            {
                ChunkSizeBytes = 64512, // 63KB
                Metadata = metaData.ToBsonDocument()
            };


در نهایت آن را آپلود میکنیم:
 var id = bucket.UploadFromBytes("GoneWithTheWind", source, options);
پارامتر اول آن، نامی برای بسته آپلودی است. پارامتر دوم، خود فایل آپلودی میباشد و با پارمتر آخر هم تنظیماتی را که برای فایل مورد نظر توسط کلاس GridFSUploadOptions  تعیین کرده‌ایم، مشخص میکنیم. موقعیکه آپلود انجام شود، به ازای این کد، یک شناسه‌ی اختصاصی از جنس ObjectId را دریافت میکنیم که می‌توانیم آن را به یک خصوصیت در سند اصلی نسبت دهیم تا ارتباط بین سند و فایل هایش را داشته باشیم.

نکته: اگر در یک کلاس، چند فیلد از جنس ObjectId دارید، مونگو در بین تشخیص شناسه اصلی سند و شناسه تصاویر، با توجه به نام خصوصیت‌ها و غیره، تا حد زیادی هوشمند عمل میکند. ولی اگر خواستید صریحا شناسه اصلی را ذکر کنید و آن را متمایز از بقیه نشان دهید، می‌توانید از خصوصیت BsonId در بالای نام فیلد ID استفاده کنید:
[BsonId]
public ObjectId Id { get; set; }

جهت خواندن فایل آپلود شده، تنها کافی است از طریق شناسه‌ی دریافتی در مرحله‌ی آپلود، اقدام نماییم:
var bytes = bucket.DownloadAsBytes(id);

نکته: تمام متدهای آپلود و دانلود دیتا، هم به صورت آرایه ای از بایت‌ها و هم به صورت استریم میتوانند مورد استفاده قرار بگیرند و به ازای هر کدام، متدهای همزمان و غیرهمزمان نیز موجود هستند.


اگر قصد دارید بر اساس نام داده شده، فایل را دریافت کنید، ممکن است که چندین فایل، تحت یک نام ذخیره شده باشند که میتوانید در حالت‌های مختلفی این تصاویر را واکشی نمایید:
0 : فایل اصلی
1: اولین نسخه فایل
2: دومین نسخه فایل
و الی آخر...

1-: جدیدترین نسخه فایل (مقدار پیش فرض)
2-: نسخه ماقبل جدیدترین نسخه فایل
و الی آخر...


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


اولین تصویر، تصویر اصلی محسوب می‌شود و بعد از آن، نسخه اول و نسخه دوم تصویر، وارد سیستم می‌شوند و تکه کد زیر از آنجاکه با مقدار پیش فرض پر شده‌است باید آخرین تصویر، تصویر سمت چپ را برای شما بر روی دیسک ذخیره کند:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var image=bucket.DownloadAsBytesByName("City of Glass-cover");
File.WriteAllBytes(@"D:\a.jpg",image);

برای مقداردهی خواص بالا به شکل زیر عمل میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var options = new GridFSDownloadByNameOptions
            {
                Revision = 0
            };
var image=bucket.DownloadAsBytesByName("City of Glass-cover",options);
File.WriteAllBytes(@"D:\a.jpg",image);
با پاس کردن مقدار 0 به مونگو، اولین تصویر وارد شده، یعنی تصویر اصلی را دریافت می‌کنیم که اولین تصویر از سمت راست می‌شود. اگر مقادیر 1 یا 1- را پاس دهیم، چون تنها سه تصویر بیشتر نیست، در هر دو حالت تصویر دوم بازگردانده می‌شود.

برای بازگردانی تصاویر از طریق مقادیر موجود در متادیتا، باید از کلاس ویژه‌ای به نام GridFSFileInfo استفاده کنیم. در اینجا هم همانند روزهای اول، از کلاس بیلدر جهت ایجاد شرط استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var filter = Builders<GridFSFileInfo>.Filter.Gte(x => x.Length , 600);
var sort = Builders<GridFSFileInfo>.Sort.Descending(x => x.UploadDateTime);
var options =new GridFSFindOptions()
            {
                Limit = 3,
                Sort = sort
            };
var cursor = bucket.Find(filter, options);
var list = cursor.ToList();
در اینجا ابتدا مشخص کرده‌ایم که فایل مورد نظر باید حجمی بیشتر از 600 بایت داشته باشد و مرتب سازی آن به صورت نزولی، بر اساس زمان آپلود باشد. در عبارت لامبدا تعریف شده، می‌توانید خصوصیت‌های مختلف یک فایل از قبیل نام، حجم (سایز) ، زمان آپلود و ... را ببینید. سپس مرتب سازی و تعداد رکورد برگشتی از ابتدای جدول را مشخص میکنیم و از متد Find یا FindAsync جهت جست‌وجو استفاده میکنیم. با شکل گرفتن کوئری درخواست، لیستی از آن را تهیه میکنیم. مقدار بازگشتی این شیء، در واقع اسنادی از تصاویر هستند که میتوانید از طریق Id یا نام آن‌ها، فایل اصلی را واکشی نمایید.
برای یافتن تصاویر بر اساس متادیتاهای تعریف شده، از کد زیر استفاده میکنیم:
var filter = Builders<GridFSFileInfo>.Filter.Eq("metadata.CoverType","Front");

تغییر نام تصاویر
جهت ویرایش یک نام فایل از طریق متدهای زیر اقدام می‌نماییم:
bucket.Rename(id, newFilename);

//یا در حالت غیرهمزمان
await bucket.RenameAsync(id, newFilename);

حذف تصویر
bucket.Delete(id);

//یا
await bucket.DeleteAsync(id);

برای حذف کل bucket از طریق کد زیر اقدام می‌نماییم:
bucket.Drop();

//یا
await bucket.DropAsync();
 
در بخش بعدی Chunk را مورد بررسی قرار می‌دهیم.