مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت چهارم
تا قسمت سوم توانستیم Xamarin را نصب و پروژه‌ی اولیه آن را بیلد کنیم. سپس کد مشترک بین سه پلتفرم را بر روی Windows اجرا و Edit & continue آن را هم تست کردیم که هم برای UI ای که با Xaml نوشته می‌شود و هم برای منطقی که با CSharp نوشته می‌شود، کار می‌کند.
همانطور که گفتیم، کد UI و Logic برای هر سه پلتفرم مشترک است؛ منتهی به علت امکانات دیباگ فوق العاده و سرعت بیشتر ویندوز، ابتدا آن را بر روی ویندوز تست کردیم و بعد برای تکمیل UI، آن را بر روی Android اجرا می‌کنیم. این بار می‌توانید دو پروژه UWP و iOS را Unload کنید و سپس پروژه Android ای را در صورت Unload بودن Load کنید. (با راست کلیک نمودن روی پروژه). این کار باعث می‌شود Visual Studio بیهوده کند نشود؛ مخصوصا اگر سیستم شما ضعیف است.
ابتدا با موبایل یا تبلت اندرویدی شروع می‌کنیم. اگر چه Xamarin از نسخه‌ی 4.0.3 اندروید به بالا را پشتیبانی می‌کند، ولی توصیه می‌کنم وقتتان را بر روی گوشی‌های اندرویدی کمتر از 4.4 تلف نکنید. دستگاه را می‌توانید، هم به صورت USB و هم به صورت Wifi استفاده کنید. ابتدا باید دستگاه اندرویدی خود را آماده‌ی برای دیباگ کنید. برای این منظور مقاله‌های فارسی و انگلیسی زیادی وجود دارند که می‌توانید از آن‌ها استفاده کنید. من عبارت "اندروید debug" را جستجو کردم و به این مقاله رسیدم. همچنین Android SDK شما باید USB debugging اش نصب شده باشد که البته حجم زیادی ندارد. برای بررسی این مورد ابتدا از وجود فولدر extras\google\usb\_driver درAndroid SDK خود مطمئن شوید. حال سؤال این است که ویژوال استودیو، Android SDK را کجا نصب کرده‌است که خیلی ساده در این لینک توضیح داده شده‌است.
اگر فولدر extras\google\usb\_driver وجود نداشت، باید آن را نصب کنید که خیلی ساده توسط Android SDK Manager امکان پذیر است؛ ولی نه! امکان پذیر نیست!
دلیل: در Xamarin شما همیشه بر روی آخرین SDK‌ها حرکت می‌کنید. این شامل Windows SDK 17134 و Android SDK 27 و iOS SDK 11 است. وقتی از نسخه‌ی فعلی ویژوال استودیو، یعنی 15.8 به نسخه‌ی بعدی ویژوال استودیو که الان Preview است بروید، یعنی 15.9، عملا به این معنا است که به Windows SDK 17763 و Android SDK 28 و iOS SDK 12 می‌روید. این بزرگترین مزیت Xamarin است و این یعنی شما همیشه به صد در صد امکانات هر پلتفرم در زبان CSharp دسترسی دارید و همیشه آخرین SDK هر سیستم عامل در اختیار شماست و اگر دوستی از طریق Swift توانست مثالی از ARKit 2.0 را در iOS 12 پیاده سازی کند، قطعا شما هم می‌توانید. همچنین تیم Xamarin نه تنها این امکانات را بلکه Documentation لازم را نیز در اختیار شما قرار می‌دهد. چون در همین مثال، مستندات Apple به زبان Swift / Objective-C بوده و مستندات Xamarin به زبان CSharp.
حال اگر سری به فولدر Android SDK نصب شده‌ی توسط Visual Studio بزنید، مشاهده می‌کنید که خبری از Android SDK Manager نیست! به صورت رسمی، مدتی است که گوگل در نسخه‌های اخیر Android SDK، دیگر Android SDK Manager را ارائه نمی‌کند و همانطور که گفتم شما الان بر روی آخرین نسخه‌ی Android SDK هستید. هر چند ترفندهایی وجود دارد که این Manager را باز می‌گردانند، ولی لزومی به انجام این کار در Xamarin نیست و شما می‌توانید از Android SDK Manager ای که تیم Xamarin ارائه داده‌است، استفاده کنید. همین مسئله در مورد Android Virtual Device Manager که برای مدیریت Emulator‌ها بود نیز صدق می‌کند.
برای استفاده از این دو، ضمن استفاده از ابزارهای دور زدن تحریم، در ویژوال استودیو، در منوی Tools، به قسمت Android رفته و Android SDK Manager را باز کنید. Android Emulator Manager نیز جایگزین Android Virtual Device Manager ای است که قبلا توسط گوگل ارائه می‌شد. حال بعد از باز کردن Android SDK Manager ارائه شده توسط Xamarin، به برگه‌ی Tools آن بروید و از  قسمت extras مطمئن شوید که Google USB driver تیک خورده باشد.
حال پس از وصل کردن گوشی یا تبلت اندرویدی به سیستم توسط کابل USB و Set as startup project نمودن پروژه‌ی XamApp.Android که در قسمت قبل آن را Clone کرده بودید، می‌توانید پروژه را بر روی گوشی خود اجرا کنید. اگر نام گوشی خود را در کنار دکمه‌ی سبز اجرای پروژه (F5) نمی‌بینید، بستن و باز کردن Visual Studio را امتحان کنید. 

پروژه را که اجرا کنید، اولین بیلد کمی طول می‌کشد (اولین بار دو برنامه بر روی گوشی شما نصب می‌شوند که برای کار دیباگ در Xamarin لازم هستند) و اساسا بیلد یک پروژه‌ی اندرویدی کند است. خوشبختانه به واسطه وجود Xaml edit and continue احتیاجی به Stop - Start کردن پروژه و بیلد کردن برای اعمال تغییرات UI نیست و به محض تغییر Xaml، می‌توانید تاثیر آن را در گوشی خود ببینید. ولی برای هر تغییر CSharp باید Stop - Start و Build کنید که زمان بر است و به همین علت تست بر روی پروژه ویندوزی را برای پیاده سازی منطق برنامه پیشنهاد می‌کنیم. البته در نسخه‌ی 15.9 ویژوال استودیو، سرعت بیلد تا 40% بهبود یافته است.
ممکن است شما گوشی اندرویدی یا تبلت نداشته باشید که بخواهید بر روی آن تست کنید و یا مثلا گوشی شما Android 7 هست و می‌خواهید بر روی Android 8 تست بگیرید. در این جا شما احتیاج به استفاده از Emulator را خواهید داشت.
توجه داشته باشید که Emulator شما ترجیحا نباید ARM باشد و بهتر است یا X86 یا X64 باشد، وگرنه ممکن است خیلی کند شود. همچنین بهتر است Google Play Services داشته باشد. همچنین ترجیحا دنبال گزینه‌ی اجرا کردن Emulator نروید؛ اگر خود ویندوز شما درون یک Virtual Machine در حال اجراست.

ابتدا ضمن جستجو کردن "فعال سازی intel virtualization"، اقدام به فعال سازی این امکان در سیستم خود کنید. این آموزش را مناسب دیدم.
گزینه‌های مطرح: [Google Android Emulator] - [Genymotion] - [Microsoft Hyper-V Android Emulator] که فقط یکی از آنها را لازم دارید.

Google Android Emulator توسط خود Google ارائه می‌شود و دارای Google Play Services نیز هست. بر اساس این آموزش به صفحه Workloads در Visual Studio Installer بروید و از قسمت Xamarin دو مورد "Google Android Emulator API Level 27" و "Intel Hardware Accelerated Execution Manager (HAXM) global install" را نصب کنید. توجه داشته باشید که بدین منظور احتیاج به ابزارهای دور زدن تحریم دارید؛ زیرا نیاز به دسترسی به سرورهای گوگل هست. این Emulator آماده برای دیباگ هست و نیازی به اقدام خاصی نیست.

Genymotion حجم کمتری دارد و برای دانلود احتیاج به ابزارهای دور زدن تحریم را ندارد و اساسا نسبت به بقیه بر روی سیستم‌های ضعیف‌تر، بهتر کار می‌کند. فقط Emulator ای که با آن می‌سازید، به صورت پیش فرض Google Play Services را ندارد که در آخرین نسخه‌های آن گزینه Open  GApps به toolbar اضافه شده که Google Play Services را اضافه می‌کند. (از انجام هر گونه عملیات پیچیده بر اساس آموزش‌هایی که برای نسخه‌های قدیمی‌تر Genymotion هستند، پرهیز کنید). مطابق با ابتدای همین آموزش برای دستگاه‌های اندرویدی، Emulator خود را آماده برای دیباگ کنید.

Microsoft Hyper-V android emulators. مایکروسافت قبلا اقدام به ارائه یک Android Emulator کرده بود که برای نسخه 4 و 5 اندروید بودند و بزرگ‌ترین ضعف آنها عدم پشتیبانی از Google Play Services بود که ادامه داده نشدند. ولی سری جدید ارائه شده توسط مایکروسافت چنین مشکلی را ندارد. اگر CPU شما AMD بوده و روش‌های قبلی برای شما کند هستند یا اساسا کار نمی‌کنند، یا در حال حاضر در حال استفاده از Docker for Windows هستید که از Hyper-V استفاده می‌کنید و قصد استفاده مجدد از منابع موجود را دارید، این نیز گزینه خوبی است که جزئیات آن را می‌توانید در  اینجا  دنبال کنید. این Emulator آماده برای دیباگ هست و نیازی به اقدام خاصی نیست. 

پس از اینکه Emulator خود را ساختید، آن را اجرا کنید. سپس برنامه را از درون ویژوال استدیو اجرا کنید. مطابق نسخه ویندوزی، دوباره یک دکمه دارید و یک Label، عدد بر روی Label، با هر بار کلیک کردن بر روی دکمه، افزایش می‌یابد.
سرعت اجرای این برنامه در Emulator یا گوشی شما برای دیباگ است و در حالت Release، سرعت چندین برابر بهتر خواهد شد و به هیچ وجه تست‌های Performance را بر روی Debug mode انجام ندهید.

حال نوبت به پابلیش پروژه می‌رسد. در این قسمت باید توجه کنید که حجم Apk شما برای پروژه‌ی XamApp مثال ما به 7 مگ می‌رسد که برای یک فرم ساده خیلی زیاد به نظر می‌رسد. ولی اگر شما بجای یک فرم ساده، صد فرم پیچیده نیز داشته باشید، باز هم این حجم به 8 مگ نخواهد رسید. حجم Apk خیلی متاثر از کدهای شما نیست، بلکه شامل موارد زیر است:
1- NET. که خود شامل CLR  & BCL است. (BCL (Base Class Library  مثل کلاس‌های string - Stream - List - File و (CLR (Common language runtime که شامل موارد لازم برای اجرای کدها است. این پیاده سازی بر اساس NET Standard 2.0. بوده که عملا اجازه استفاده از تعداد خیلی زیادی از کتابخانه‌های موجود را می‌دهد، حتی Entity framework core! البته هر کتابخانه حجم DLL‌های خودش را اضافه می‌کند.
2- Android Support libraries که به شما اجازه می‌دهد از تعداد زیادی (و البته نه همه) امکانات نسخه‌های جدید اندروید در پروژه‌تان استفاده کنید که بر روی نسخ قدیمی‌تر Android نیز کار کنند. همچنین با یکپارچگی با Google Play Services عملا خیلی از کارها ساده‌تر و با Performance بهتری انجام می‌شود، مانند گرفتن موقعیت کاربر جاری.
3-  Xamarin essentials . اگر چه در CSharp شما به صد در صد امکانات هر سیستم عامل دسترسی دارید و می‌توانید مثلا مقدار درصد شارژ باطری را بخوانید، ولی اینکار مستلزم نوشتن سه کد CSharp ای برای Android - iOS - Windows است که طبیعتا کار را سخت می‌کند. اما Xamarin Essentials به شما اجازه می‌دهد با یک کد CSharp واحد برای هر سه پلتفرم، با باطری، کلیپ‌بورد، قطب نما و خیلی موارد دیگر کار کنید.
4- Xamarin.Forms. اگر Button و Label ای که در مثال برنامه داشتیم، با یکبار نوشتن بر روی هر سه پلتفرم دارند کار می‌کنند، در حالی که هر پلتفرم، Button مخصوص به خود را دارد؛ این را Xamarin Forms مدیریت می‌کند. علاوه بر این، Binding نیز به عهده‌ی Xamarin Forms است.
5- Prism Autofac Bit Framework: درک آن‌ها نیاز به دنبال کردن آموزش‌های این دوره را دارد؛ ولی به صورت کلی معماری پروژه شما بسیار کارآمد و حرفه‌ای خواهد شد و به کدی با قابلیت نگهداری بالا خواهید رسید. 
6-  Rg Plugins Popup  و  Xamanimation  نیز دو کتابخانه‌ی UI بسیار کاربردی و جالب هستند که در طول این آموزش از آنها استفاده خواهد شد.
حجم 7 مگ برای این تعداد کتابخانه و امکان، خیلی زیاد نیست و شما عملا تعداد زیادی از پروژه‌های خود را می‌توانید با همین حجم ببندید و اگر مثلا به پروژه‌ی Humanizer خیلی علاقه داشته باشید (که در این صورت حق هم دارید!) می‌توانید با اضافه شدن چند کیلوبایت (!) به پروژه آن را داشته باشید. اکثر کتابخانه‌های NET. ای سبک هستند. همچنین موقع قرار گرفتن در پروژه، فشرده سازی نیز می‌شوند و قسمت‌های استفاده نشده‌ی آن‌ها نیز توسط Linker حذف می‌شوند.
علاوه بر این، اجرای برنامه بر روی گوشی‌های ضعیف و قدیمی کمی طول می‌کشد. این مربوط به اجرای برنامه است؛ نه باز شدن فرم مثال ما که دارای Button و Label بود و اگر مثال ما دو فرم داشته باشد (که در آموزش‌های بعدی به آن می‌رسیم) می‌بینید که چرخش بین فرم‌ها بسیار سریع است.

مواردی مهم در زمینه‌ی بهبود عملکرد پروژه‌های Xamarin در Android
در ابتدا باید بدانید Apk شما شامل دو قسمت است؛ یکی کدهای CSharp ای شما که DotNet ای بوده و در کنار کدهای کتابخانه‌هایی چون Json.NET بر روی DotNet اجرا می‌شوند. دیگری کتابخانه‌ای است که مثلا با Java نوشته شده و بعد برای استفاده در CSharp بر روی آن یک Wrapper یا پوشاننده توسعه داده شده‌است. عموما توسعه دهندگان چنین پروژه‌هایی، ابتدا پروژه را به Java می‌نویسند و بعد برای JavaScript - CSharp و ... Wrapper ارائه می‌دهند.
برای بهبود اینها ابزارهایی چون AOT-NDK-LLVM-ProGurad-Linker و ... وجود دارند که سعی می‌کنم به صورت ساده آنها را توضیح دهم.

وظیفه ProGurad این است که از قسمتی از پروژه‌ی شما که بخاطر کتابخانه‌های Java ای، عملا DotNet ای نیست، کدهای اضافه و استفاده نشده را حذف کند.
ممکن است استفاده از ProGurad باعث شود کلاسی که داینامیک استفاده شده است، به اشتباه حذف شود. پروژه XamApp دارای یک ProGuard configuration file است که جلوی چنین اشتباهاتی را حتی الامکان می‌گیرد.
همچنین ProGurad که در داخل Android SDK قرار دارد، به Space در طول مسیر حساس است (!) و با توجه به اینکه مسیر پیش فرض Android SDK نصب شده‌ی توسط ویژوال استودیو دارای Space است (C:\Program Files (x86)\Android\android-sdk)  شما در همان ابتدا دچار مشکل می‌شوید! برای حل این مشکل ابدا فولدر Android SDK را جا به جا نکنید؛ بلکه از امکانی در ویندوز به نام Junction folder یا فولدر جانشین استفاده کنید. بدین منظور دستور زیر را وارد کنید:
mklink /j C:\android-sdk "C:\Program Files (x86)\Android\android-sdk"
این مورد باعث می‌شود که مسیر C:\android-sdk نیز به همان مسیر پیش فرض اشاره کند و این دو مسیر در واقع یکی هستند. امیدوارم این امکان را با قابلیت Shortcut سازی در ویندوز اشتباه نگیرید! حال از منوی Tools > Options > Xamarin > Android Settings مسیر Android SDK را به C:\android-sdk تغییر دهید که فاقد Space است و ویژوال استودیو را ببندید و باز کنید.

NDK که در ادامه SDK برای Android قرار می‌گیرد، Native Development Kit است و باعث می‌شود هم DLL‌های DotNet ای و هم Jar‌های Java ای به فایل‌های so تبدیل شوند. so همان DLL ویندوز است، البته برای Linux و همانطور که احتمالا می‌دانید، پایه Android بر روی Linux است. طبیعتا کامپایل شدن کدها به so، بر روی بهبود سرعت برنامه تاثیر گذار است.

Linker نیز مشابه با ProGuard کمک می‌کند، ولی اینبار حجم DLL‌های DotNet ای مانند Json.NET را کم می‌کند. بالاخره شما از صد در صد کلاس‌های یک DLL استفاده نمی‌کنید و موارد اضافی نیز باید حذف شوند. البته این وسط، امکان حذف اشتباه کلاس‌هایی که به صورت داینامیک فراخوانی شده باشند وجود دارد که LinkerConfig موجود در پروژه XamApp حتی الامکان جلوی این مشکل را می‌گیرد.

Release mode  مثل هر پروژه CSharp ای دیگری، بهتر است پروژه در حالت Release mode پابلیش شود. در پروژه XamApp در حالت Release mode، موارد بالا یعنی Linker-NDK-ProGuard نیز درخواست می‌شوند.

جزئیات این موارد در مستندات Xamarin وجود دارد و در پایان این دوره یک Project Builder سورس باز نیز به شما ارائه می‌شود که ساختار اولیه پروژه‌ها را بر اساس نیازهای شما و با بهترین تنظیمات ممکن می‌سازد.

در پروژه XamApp علاوه بر موارد فوق، دو مورد دیگر نیز آماده به استفاده هستند، ولی غیر فعال شده اند؛ AOT و LLVM. اگر به تازگی برنامه نویس شده‌اید، موارد زیر ممکن است خیلی برایتان پیچیده باشند، از آن‌ها عبور کنید و به عنوان "نحوه انجام دادن پابلیش" بروید.

کدهای‌های DotNet ای به سه شکل می‌توانند اجرا شوند:
JIT - AOT - Interpreter
یک برنامه DotNet ای برای اجرا می‌تواند از ترکیب اینها استفاده کند. حالت Interpreter که خیلی جدید معرفی شده و الآن موضوع بحث نیست؛ می‌ماند JIT و AOT
کد CSharp در هنگام کامپایل به IL تبدیل و سپس در زمان اجرا توسط Just in time compiler به زبان ماشین تبدیل می‌شود. اگر قبلا پروژه‌ی ASP.NET یا ASP.NET Core نوشته باشید، چنین رفتاری را در پشت صحنه خواهد داشت. خود JIT که در هر بار اجرای برنامه انجام می‌شود، عملا زمان بر هست. ولی کد زبان ماشین حاصل از آن خیلی Optimize شده برای دقیقا همان ماشین هست؛ با در نظر گرفتن خیلی فاکتورها. در پروژه‌های سمت سرور مثل ASP.NET که پروژه وقتی یک بار اجرا می‌شود، مثلا روی IIS، ممکن است صدها هزار دستور را اجرا کند، در طول چندین روز یا ماه، این عمل JIT خیلی مفید هست. البته همان سربار اولیه‌ی JIT هم توسط چیزی به عنوان Tiered JIT می‌تواند کمتر شود.
اما در پروژه‌ی موبایل که برنامه ممکن است بعد از باز شدن، مثلا ده دقیقه باز باشد و بعد بسته شود، انجام شدن JIT با هر بار باز شدن برنامه خیلی مفید به فایده نیست. بنا به برخی مسائل که واقعا سطح این آموزش را خیلی پیچیده می‌کند، نتیجه کار JIT قابلیت Cache شدن آن چنانی ندارد و عملا باید هر بار اجرا شود.
در پروژه‌های موبایل، گزینه دیگری بر روی میز هست به نام Ahead of time یا AOT که کار تبدیل IL به زبان ماشین را در زمان کامپایل و پابلیش پروژه انجام دهد. طبیعتا این باعث می‌شود سرعت برنامه موبایل در عمل خیلی بالاتر رود، چون سربار JIT در هر بار اجرای برنامه حذف می‌شود. همچنین روال AOT می‌تواند از LLVM یا Low level virtual machine استفاده کند که منجر به تبدیل شدن کد زبان ماشینی می‌شود که بر روی LLVM کار می‌کند. LLVM خودش یک Runtime با سرعت خیلی بالاست که بر روی تمامی سیستم عامل‌ها کار می‌کند.
بر روی Android - iOS - Windows می‌شود از AOT استفاده کرد. در iOS و ویندوز، استفاده‌ی از AOT منجر به افزایش سایز برنامه نمی‌شود، چون قبلا برنامه یک سری کد IL بوده که زمان اجرا توسط JIT به کد ماشین تبدیل می‌شده و الان بجای آن IL، یک سری کد زبان ماشین مبتنی بر LLVM هست. اما بر روی Android، پیاده سازی AOT ناقص هست و البته که با فعال کردن‌اش، سرعت برنامه بسیار بیشتر می‌شود، ولی کماکان نیاز به JIT و IL هم برای برخی از سناریوها هست. این مورد یعنی اینکه فعال سازی AOT+LLVM بر روی اندروید تا مادامی که AOT در Android به صورت آزمایشی هست، باعث افزایش حجم Apk ما از 7 به 13 مگ می‌شود. البته این مورد در نسخه‌های بعدی رفع خواهد شد و رفتار Android مشابه با iOS-Windows خواهد بود؛ یعنی حجم نسبتا کم و سرعت خیلی بالا.
برای فعال سازی AOT+LLVM در csproj پروژه اندرویدی، دو مقدار AotAssemblies و EnableLLVM را از false به true تغییر دهید:
 <AotAssemblies>true</AotAssemblies> 
 <EnableLLVM>true</EnableLLVM>
با این تنظیمات، بیلد شما طولانی‌تر و در عوض سرعت اجرای برنامه بیشتر خواهد شد.

نحوه انجام دادن پابلیش 
برای انجام دادن پابلیش، بر روی پروژه XamApp.Android در هنگامیکه بر روی Release mode هستید، راست کلیک کنید و Archive را بزنید. سپس فایل Archive شده را انتخاب و Distribute را بزنید که به شما Apk مناسب برای انتشار توسط خودتان یا Google Play می‌دهد.
نکات مهم:
1- فایل Apk حاصل از Archive را بدون Distribute کردن، در اختیار کسی قرار ندهید. فقط پیام Corrupt و خراب بودن فایل، حاصل کارتان خواهد بود!
2- اولین باری که Distribute می‌کنید، Wizard مربوطه کمک می‌کند تا یک فایل Certificate را برای Apk اتان بسازید. آن فایل را گم نکنید! در پابلیش‌های بعدی دیگر نباید Certificate جدیدی بسازید؛ بلکه فایل قبلی را باید به آن معرفی کنید و فقط رمز آن Certificate را دوباره بزنید.
3- به برنامه آیکون بدهید. برای آن Splash Screen خوبی بگذارید. در هر بار پابلیش، ورژن برنامه را افزایش دهید. اینها همگی توضیحات اش بر روی بستر وب موجود است. سؤالی بود، همینجا هم می‌توانید بپرسید.
فایل‌های Apk این مثال را می‌توانید از اینجا دانلود کنید.

در قسمت بعدی آموزش، دیباگ و پابلیش گرفتن پروژه بر روی iOS را خواهیم داشت که البته مقداری از مطالب اش با مطالب این آموزش مشترک هست. بعد دست به کد شده و آموزش CSharp و Xaml را خواهیم داشت تا پروژه‌ای با کیفیت، کارآمد و عالی از هر جهت بنویسید.
همچنین تعدادی از نکات مربوط به Performance که مربوط به ظاهر برنامه و نحوه چیدمان صفحات و کنترل‌ها هستند و بر روی Performance هر سه پلتفرم تاثیر گذار هستند (و نه فقط Android‌) نیز در ادامه بحث خواهند شد.
نظرات مطالب
صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
سلام و ممنون از راهنمایی‌های خوبتان
کتابخانه اصلی Lib.AspNetCore.Mvc.JqGrid مربوط به پروژه Demo.AspNetCore.JqGrid رو با نسخه ASP.NET Core 3.1 به روز رسانی کردم و بعضی خطاهای Type Load Exception هم که در سورس فریمورک اصلی بود برطرف کردم. چون کدهای سورس اصلی فریمورک پروژه رو مجبور شدم تغییر بدم دیگه نشد که به صورت nuget Package به پروژه اضافه کنم برای همین سورس فریمورک اصلی رو در یک پوشه در کنار پروژه دمو که همه را به نسخه ASP.NET Core 3.1 به روز رسانی کردم گذاشتم. البته این نسخه با همان شیوه ای هست که در این لینک توضیح داده بودم و هم میشه با انواع بانکهای اطلاعاتی NoSQL استفاده کرد و هم با بانک اطلاعاتی‌های رابطه ای از قبیل MS SQL Server.

ضمنا اون موارد خطا هم به خاطر همین exception داخلی Type Load Exception در فریمورک اصلی بود که پس از ارتقا به نسخه 3.1 برطرف شد.


دوستان از لینک زیر می‌توانید نسخه بروز رسانی شده با ASP.NET Core 3.1 را استفاده نمایید.
Demo.JqGrid.core3.1.rar  (6.25 MB) 




در لینک هم همون نسخه دمو به همراه سورسهای فریمورک اصلی رو که تغییر دادم برای نویسنده اصلی فریمورک Lib.AspNetCore.Mvc.JqGrid در کامنت بارگزاری کردم تا فریمورک اصلی رو هم بروز رسانی کنند تا بعدا بشه به صورت nuget package استفاده کرد. البته توی اون نسخه کتابخانه JqGrid.Core که در نمونه مثال اینجا آپلود کردم نیست به این دلیل که نمونه مثال خودشون رو داشته باشند و باعث پیچیدگی کد برای ارتقای package نشه


سورس پروژه رو با این تغییرات در گیتهاب هم منتشر کردم

مطالب
آموزش 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، کش را به شیوه‌ای متفاوت در لایه‌های مختلفی از برنامه قرار داده‌ام تا شما همراهان بتوانید طبق نیازتان از روشی مناسب و بهینه در پروژه‌های واقعی خود از آن استفاده کنید.
مطالب
نوع‌های نال نپذیر در TypeScript
تا پیش از ارائه‌ی کامپایلر TypeScript 2.0، مقادیر null و undefined، به هر نوعی قابل انتساب بودند و امکان تفکیک آن‌ها وجود نداشت که این مورد می‌تواند منشاء بروز بسیاری از خطاهای در زمان اجرا شود.
let name: string;
name = "Vahid"; // OK
name = null; // OK
name = undefined;  // OK
let age: number;
age = 24; // OK
age = null; // OK
age = undefined;  // OK
برای نمونه در اینجا یک متغیر رشته‌ای و همچنین عددی تعریف شده‌اند که انتساب null و یا undefined نیز به آن‌ها مجاز است. این مورد جهت نوع‌های ورودی و خروجی متدها، اشیاء و آرایه‌ها نیز میسر است.


نوع null در TypeScript

همانند JavaScript، نوع null تنها یک مقدار معتبر نال را می‌تواند داشته باشد و نمی‌توان برای مثال یک رشته را به آن انتساب داد. اما انتساب این مقدار به هر نوع متغیر دیگری، سبب پاک شدن مقدار آن خواهد شد. با فعالسازی strictNullChecks، این نوع را تنها به نوع‌های نال‌پذیر می‌توان انتساب داد.


نوع undefined در TypeScript

هر متغیری که مقداری به آن انتساب داده نشده باشد، با undefined مقدار دهی می‌شود. این مورد حتی جهت خروجی متدها نیز صادق است و اگر return ایی در آن‌ها فراموش شود، این خروجی نیز به undefined تفسیر می‌شود.
در اینجا نیز اگر نوع متغیری به undefined تنظیم شد، این متغیر تنها مقدار undefined را می‌تواند بپذیرد. تنها با خاموش کردن پرچم strictNullChecks می‌توان آن‌را به اعداد، رشته‌ها و غیره نیز انتساب داد.


فعالسازی نوع‌های نال نپذیر در TypeScript

برای فعالسازی این قابلیت، نیاز است پرچم strictNullChecks را در فایل تنظیمات کامپایلر به true تنظیم کرد:
{
    "compilerOptions": {
        "strictNullChecks": true
    }
}
از این پس دیگر نمی‌توان null و undefined را به هر نوعی انتساب داد و این‌ها تنها به خودشان و یا نوع any، قابل انتساب هستند. برای مثال اکنون نوع number فقط یک عدد است و دیگر قابلیت پذیرش null و یا undefined را ندارد. البته در اینجا یک استثناء هم وجود دارد: undefined را می‌توان به نوع void نیز انتساب داد.
برای مثال اگر متدی، رشته‌ای را به عنوان پارامتر قبول کند، تا پیش از TypeScript 2.0 و فعالسازی strictNullChecks آن، مشخص نبود که رشته‌ی دریافتی از آن واقعا یک رشته‌است و یا شاید null. اما اکنون یک رشته، فقط یک رشته‌است و دیگر نال پذیر نیست.
 let foo: string = null; // Error! Type 'null' is not assignable to type 'string'.
به این ترتیب دیگر به خطاهای زمان اجرایی مانند خطاهای ذیل نخواهیم رسید:
Uncaught ReferenceError: foo is not defined
Uncaught TypeError: window.foo is not a function

این مورد برای آرایه‌ها نیز صادق است:
// With strictNullChecks set to false
let d: Array<number> = [null, undefined, 10, 15]; //OK
let e: Array<string> = ["pie", null, ""];  //OK
 
 
// With strictNullChecks set to true
let d: Array<number> = [null, undefined, 10, 15]; // Error
let e: Array<string> = ["pie", null, ""]; // Error
اگر strictNullChecks فعال شود، دیگر نمی‌توان به اعضای یک آرایه مقادیر null و یا undefined را نسبت داد.


ساده سازی تعریف بررسی‌های با پرچم strict، در TypeScript 2.3

تعداد گزینه‌های قابل تنظیم در فایل tsconfig روز به روز بیشتر می‌شوند. به همین جهت برای ساده سازی فعالسازی آن‌ها، از TypeScript 2.3 به بعد، پرچم strict نیز به این تنظیمات اضافه شده‌است. کار آن فعالسازی یکجای تمام بررسی‌های strict است؛ مانند noImplicitAny، strictNullChecks و غیره.
{ 
    "compilerOptions": { 
        "strict": true  /* Enable all strict type-checking options. */ 
    } 
}
در این حالت اگر نیاز به لغو یکی از گزینه‌ها بود، می‌توان به صورت ذیل عمل کرد:
{ 
    "compilerOptions": { 
        "strict": true, 
        "noImplicitThis": false 
    } 
}
گزینه‌ی strict تمام بررسی‌های متداول را فعال می‌کند؛ اما ذکر و تنظیم صریح noImplicitThis به false، تنها این یک مورد را لغو خواهد کرد.

یک نکته: اجرای دستور tsc --init ، سبب تولید یک فایل tsconfig.json از پیش تنظیم شده، بر اساس آخرین قابلیت‌های کامپایلر TypeScript می‌شود.


اما ... اکنون چگونه یک نوع را نال‌پذیر کنیم؟

TypeScript به همراه دو نوع ویژه‌ی null و undefined نیز شده‌است که تنها دارای مقادیر null و undefined می‌توانند باشند. به این معنا که در حین تعریف نوع یک متغیر، می‌توان این دو را نیز ذکر کرد و دیگر تنها به عنوان دو مقدار مطرح نیستند. به این ترتیب می‌توان از آن‌ها یک union type را ایجاد کرد:
 let foo: string | null = null; // Okay!
اکنون تنها در این حالت است که متغیر foo می‌تواند یک رشته و یا یک null را دریافت کند و یا اگر مثال ابتدای بحث را بخواهیم اصلاح کنیم، به نمونه‌ی ذیل خواهیم رسید:
let name: string | null;
name = "Vahid"; // OK
name = null; // OK
name = undefined;  // Error
یکی دیگر از مزایای این روش، وضوح بیشتر تعریف نوع متغیرها و به نوعی «خود مستند سازی» بهتر آن‌ها است. در این حالت یا به صورت صریح مشخص می‌کنیم که متدی فقط یک رشته را می‌پذیرد و یا با ذکر string | null، به استفاده کننده اعلام می‌کنیم که ارسال null نیز به آن پیش بینی شده‌است و به نتیجه‌ی نامشخصی منتهی نخواهد شد.

یک نکته:
تا پیش از این اگر متغیری را به این صورت تعریف می‌کردیم:
let z = null;
نوع آن any درنظر گرفته می‌شد. اما اکنون، نوع آن تنها null است و تنها مقداری را هم که می‌تواند بپذیرد نال خواهد بود.


بررسی انتساب، پیش از استفاده

با فعالسازی strictNullChecks، اکنون کامپایلر برای تمام نوع‌هایی که undefined نیستند، یک مقدار اولیه را پیش از استفاده‌ی از آن‌ها درخواست می‌کند:
testAssignedBeforeUseChecking() {
    let x: number;
    console.log(x);
}
در اینجا چون x از نوع عددی است، به علت عدم مقدار دهی اولیه، قابلیت استفاده‌ی از آن وجود ندارد و کامپایلر خطای ذیل را اعلام می‌کند:
 [ts] Variable 'x' is used before being assigned.

اما در حالت ذیل، عدد z می‌تواند عدد و یا undefined باشد؛ به همین جهت کامپایلر با استفاده‌ی از آن مشکلی نخواهد داشت:
let z: number | undefined;
console.log(z);

یک نکته: خواص و پارامترهای اختیاری، به صورت خودکار دارای نوع undefined نیز هستند. برای مثال امضای متد ذیل:
method1(x?: number) {
}
با متد زیر یکی است:
method1(x?: number | undefined) {
}


اجبار به بررسی نال نبودن مقادیر، پیش از استفاده‌ی از آن‌ها در متدهای نال نپذیر

اگر پارامتر متدی یا خاصیت شیءایی نال پذیر نباشند، با ارسال مقدار نوعی به آن‌ها که می‌تواند null و یا undefined را بپذیرد، یک خطای زمان کامپایل صادر خواهد شد. در اینجا محافظ‌های نوع‌ها توسعه یافته‌اند تا اگر بررسی نال یا undefined بودن مقداری انجام شد، مشکلی در جهت استفاده‌ی از آن‌ها نباشد:
  f(x: number): string {
    return x.toString();
  }

  testTypeGuards() {
    let x: number | null | undefined;
    if (x) {
      this.f(x);  // Ok, type of x is number here
    } else {
      this.f(x);  // Error, type of x is number? here
    }
  }
در این مثال، متد f فقط یک عدد را می‌پذیرد (و نه نال و یا undefined). اما در حین کاربرد آن در متد testTypeGuards، مقدار متغیر x می‌تواند یک عدد، نال و یا undefined باشد. چون پیش از اولین استفاده‌ی از متد f در اینجا، بررسی دارای مقدار بودن این متغیر صورت گرفته‌است، فراخوانی صورت گرفته، مجاز است. اما در قسمت else این شرط، کامپایلر خطای ذیل را صادر می‌کند:
 Argument of type 'number | null | undefined' is not assignable to parameter of type 'number'.
Type 'undefined' is not assignable to type 'number'.

امکان این بررسی در مورد عبارات شرطی نیز صادق است:
getLength(s: string | null) {
   return s ? s.length : 0;
}


توسعه‌ی محافظ‌های نوع‌ها جهت کار با نوع‌های نال نپذیر

در مثال ذیل، خروجی متد isNumber دارای امضایی به همراه is است:
isNumber(n: any): n is number { // type guard
   return typeof n === "number";
}
به یک چنین متدهایی type guard گفته می‌شود که امکان بررسی یک نوع را میسر می‌کنند. از این امکان می‌توان جهت بررسی بهتر پارامترها و یا خواص اختیاری استفاده کرد:
  usedMb(usedBytes?: number): number | undefined {
    return this.isNumber(usedBytes) ? (usedBytes / (1024 * 1024)) : undefined;
  }
یک چنین بررسی، بهتر است از بررسی ذیل:
  usedMb2(usedBytes?: number): number | undefined {
    return usedBytes ? (usedBytes / (1024 * 1024)) : undefined;
  }
از این جهت که عبارت شرطی بررسی شده، مقدار صفر را نیز به صورت undefined بازگشت خواهد داد (if(0) به false تعبیر می‌شود و قسمت else این شرط فراخوانی خواهد شد).
همچنین امضای متد نیز به number | undefined تغییر یافته‌است. در غیر اینصورت، خطای زمان کامپایل Type undefined is not assignable to type number صادر خواهد شد.
در حین استفاده‌ی از یک چنین متدی، دیگر نمی‌توان به خروجی آن به صورت ذیل دسترسی یافت:
  formatUsedMb(): string {
    //ERROR: TS2531: Object is possibly undefined
    return this.usedMb(123).toFixed(0).toString();
  }
چون مقدار usedMb می‌تواند undefined باشد، باید ابتدا آن‌را بررسی کرد:
  formatUsed(): string {
    const usedMb = this.usedMb(123);
    return usedMb ? usedMb.toFixed(0).toString() : "";
  }


لغو بررسی strictNullChecks به صورت موقت

با استفاده از اپراتور ! می‌توان به کامپایلر اطمینان داد که این متغیر یا خاصیت، دارای مقدار نال نیست و نخواهد بود:
export interface User {
  name: string;
  age?: number;
}
در این اینترفیس، خاصیت age به صورت اختیاری تعریف شده‌است. برای نمایش مقدار age با فعال بودن strictNullChecks، یا باید ابتدا null نبودن آن‌را به صورت صریحی بررسی کرد:
  printUserInfo(user: User) {
    if (user.age != null) {
      console.log(`${user.name}, ${user.age.toString()}`);
    }
  }
در غیراینصورت قطعه کد ذیل با خطای 'Object is possibly 'undefined کامپایل نخواهد شد:
  printUserInfo(user: User) {
    console.log(`${user.name}, ${user.age.toString()}`);
  }

و یا می‌توان توسط اپراتور ! این بررسی را به صورت موقت خاموش کرد:
  printUserInfo(user: User) {
    console.log(`${user.name}, ${user.age!.toString()}`);
  }
البته استفاده‌ی از این اپراتور توسط tslint توصیه نمی‌شود:
 [tslint] Forbidden non null assertion (no-non-null-assertion)
چون بهتر است به کامپایلر عنوان نکنیم «قسم می‌خورم که این مقدار نال نیست»!



یک نکته‌ی تکمیلی
پس از آزمایش موفقیت آمیز نوع‌های نال نپذیر در TypeScript، مایکروسافت قصد دارد این ویژگی را به C# 8.0 نیز در مورد نوع‌های ارجاعی که می‌توانند نال پذیر باشند، اضافه کند (امکان داشتن نوع‌های ارجاعی نال‌نپذیر).
نظرات مطالب
تفاوت Desktop Application با Web Application
به نظر من این بحث به همین سادگی نیست و انتخاب پلتفرم اجرای پروژه به پارامترها و ویژگی‌های زیادی مرتبط هست. بطور مثال سرعت توسعه برنامه‌های ویندوز حداقل در قسمت طراحی رابط کاربری سریعتر و ساده‌تر از وب هست. و یا در مثال دیگر رفتار غیر یکسان مرورگرها مشکلاتی را در طراحی نرم افزار‌های بزرگ ایجاد می‌کنه و مشکل ساز میشه من بعد از سال‌ها طراحی سیستم‌های سازمانی روش استفاده ترکیبی از پلتفرم‌های مختلف را انتخاب کردم بطور مثال قسمت مدیریت یک سیستم را بصورت ویندوزی و قسمت رابط ماربری را با وب و... طراحی کردم. متاسفانه طراحی اولیه زبان HTML با هدف نمایش اطلاعات بوده و بهبود‌های اخیر از جمله وب 2 پاسخی منطقی به نیاز به توسعه نرم افزارهای Cross Platform بوده ولی هنوز هم با پیچیدگی‌های زیادی روبروست. به نظر قابلیت‌های نرم افزار تحت وب بیش از واقعیت بزرگ نمایی شده و هنوز هم در برخی راه کار‌ها استفاده از نرم افزار‌های تحت ویندوز گزینه مناسب‌تری خواهد بود اما این به معنی چشم پوشی بر مزایای منحصر به فرد وب نخواهد بود و هنوز انتخاب پلتفرم بستگی زیادی به نیازمندی‌ها و امکانات پروژه خواهد داشت. 
مطالب
اضافه کردن OData به پروژه‌های ASP.NET Core 3.1 با اضافه کردن فقط 20 کلمه به کد!
به مناسبت ارائه‌ی نسخه 7.4 از Microsoft.AspNetCore.OData که دیروز صورت پذیرفت، تصمیم گرفتم آموزش استفاده از OData را در پروژه‌های ASP.NET Core 3.1 به بالا که دارای endpoint routing هستند (روش توصیه شده)، تهیه کنم تا در آن، پروژه کمترین تغییر ممکن را برای اضافه شدن OData داشته باشد و ببینیم که استفاده از آن در نسخه‌های جدید، به چه میزان آسان شده است.

ابتدا با dotnet --version و یا dotnet --info و یا هر روش دیگری، از نصب بودن dot net core 3.1 sdk مطمئن می‌شویم. سپس دستور
dotnet new webapi -o SampleApi
را می‌زنیم.

در ویژوال استودیو، این دستور معادل ساخت پروژه‌ای جدید از نوع ASP.NET Core Web Application است که در دیالوگ بعدی، از بین گزینه‌های Empty، Api و Web Application و ... ما گزینه‌ی Api را انتخاب می‌کنیم.
این یک پروژه‌ی Web Api و با استفاده از endpoint routing است. در این پروژه یک WeatherForecast وجود دارد که نقش مدل را ایفا می‌کند و یک WeatherForecastController که در Get خود، تعدادی از WeatherForecastها را ساخته و باز می‌گرداند. این پروژه فاقد دیتابیس است.

حال چه کنیم که این پروژه OData enabled شود؟
1- دو Package زیر را نصب می‌کنیم:
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.3" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.4.0" />

2- در Startup.cs ابتدا
using Microsoft.AspNet.OData.Extensions;
را در بالای فایل اضافه کرده، و کدهای ConfigureServices را به شکل زیر تغییر می‌دهیم:
services.AddControllers().AddNewtonsoftJson(); // Add .AddNewtonsoftJson() to services.AddControllers();
services.AddOData(); // add this new line
همچنین کد app.UseEndpoints را به صورت زیر تغییر می‌دهیم:
app.UseEndpoints(endpoints => // Existing code
{   
    endpoints.MapControllers(); // Existing code
    endpoints.EnableDependencyInjection(); // Add this new line
    endpoints.Select().Expand().Filter().OrderBy().Count().MaxTop(20); // Add this new line
});

سپس در WeatherForecastController و متد Get، کد را به شکل زیر تغییر دهید:
// replace:
[HttpGet] // Existing code  
// with:
[HttpGet, EnableQuery] // new code
برنامه را اجرا کنید و آدرس زیر را بزنید:
وجود orderby=TemperatureC باعث می‌شود که WeatherForcastها، مرتب شده بر اساس درجه سانتیگرادی که دارند، برای کلاینت ارسال شوند.

همانطور که مشاهده کردید، به ساده‌ترین شکل ممکن، OData به پروژه اضافه شد!

به صورت کلی OData امکان فیلتر کردن رکوردهای بازگشتی، Projection، مرتب سازی، Paging، گروه بندی، Aggregation و ... را دارد که در ادامه چند مثالی را با هم می‌بینیم. 
فقط با توجه به اینکه مثال پیش فرض ASP.NET Core، یعنی WeatherForecast کمی گنگ و غیر متداول است، از آن میگذریم و با فرض لیستی از مشتریان با ساختار زیر پیش می‌رویم:
public class Customer
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public Gender Gender { get; set; }

    public AddressInfo Address { get; set; }
}

public class AddressInfo
{
    public int StreetNo { get; set; }

    public string PostalCode { get; set; }
}

public enum Gender
{
    Man, Woman, Other
}  
برای بازگردادن مشتریان خانم، داریم: (آموزش فیلتر بر روی enum)
?$filter=Gender eq 'Woman'
برای مرتب سازی مشتریان خانم بر اساس شماره خیابان آدرس آنها: (آموزش مرتب سازی بر روی Nested Properties و ترکیب orderby و filter)
?$filter=Gender eq 'Woman'&$orderby=Address/StreetNo
برای بازگرداندن مشتریانی که در اسم آنها کلمه‌ی ali وجود دارد:
?$filter=contains(FirstName,'Ali')
برای مشاهده لیست امکانات کوئری گیری و مثال‌های بیشتر می‌توانید به این لینک مراجعه کنید.

امکان کوئری گیری به صورت مفصل وجود دارد و می‌توانید مواردی چون Any / All را نیز در فیلترهای خود بگنجانید.
به علاوه اگر به جای یک آرایه یا لیست، یک IQueryable از EntityFrameworkCore گرفته و ... بازگردانید، OData کوئری ارسالی را روی IQueryable مربوطه اعمال و paging و orderby و ... تماما روی دیتابیس انجام و فقط دیتای لازم و مورد نیاز از دیتابیس خوانده شده و به کلاینت باز می‌گردد که این مهم در بهبود عملکرد برنامه بسیار موثر است.

نکته مهم این است که در این روش، امنیت برنامه به خطر نمیافتد؛ زیرا اگر شما بر اساس منطق برنامه، یک Where را سمت سرور اعمال کنید، Client فقط میتواند روی دیتایی که حق دارد ببیند Paging و OrderBy و... اعمال کند، نه اینکه دیتای بیشتری را از سرور دریافت کند.
همچنین در این روش، استفاده از Swagger، Routing و ... تماما به روشی است که همین الان آن را بلدید، در کنار رعایت best practiceهایی چون بازگرداندن Dto به جای Entity و ... نیز کاملا امکان پذیر است.
اشتراک‌ها
مصاحبه‌ای با مهندس ارشد ماشین لرنینگ در توییتر

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

مصاحبه‌ای با مهندس ارشد ماشین لرنینگ در توییتر
مطالب
آموزش TypeScript #2
در این پست قصد دارم به بررسی چند نکته که از پیش نیاز‌های کار با TypeScript  است بپردازم. همان طور که در پست قبلی مشاهده شد بعد از دانلود و نصب افزونه  TypeScript در VS.Net یک Template به نام Html Application With TypeScript به Installed Template اضافه خواهد شد. بعد از انتخاب این قسمت شما به راحتی می‌توانید در هر فایل با پسوند ts کد‌های مورد نظر به زبان TypeScript را نوشته و بعد از build پروژه این کد‌ها تبدیل به کد‌های JavaScript خواهند شد. بعد کافیست فایل مورد نظر را با استفاده از تک Script در فایل خود رفرنس دهید. دقت کنید که پسوند فایل حتما باید js باشد(به دلیل اینکه بعد از build پروژه فایل‌های ts تبدیل به js می‌شوند).
برای مثال:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script src="app.js"></script>
</head>
<body>
    <h1>Number Type in TypeScript</h1>
    <div id="content"/>
</body>
</html>

اما اگر یک پروژه وب نظیر Asp.Net MVC داشته باشیم و می‌خواهیم یک یا چند فایل که حاوی کد‌های typeScript هستند را به این پروژه اضافه کرده و از آن‌ها در صفخات وب خود استفاده کنیم باید به این صورت عمل نمود:
بعد از اضافه کردن فایل‌های مورد نیاز، پروژه مورد نظر را Unload کنید. بعد به صورت زیر فایل پروژه(csproj) رو با یک ویرایشگر متنی باز کنید:

در این مرحله باید دو قسمت اضافه شود. یک بخش  ItemGroup است که هر فایلی که در پروژه شما دارای پسوند ts است باید در این جا تعریف شود. در واقع این قسمت فایل هایی را که باید کامپایل شده تا در نهایت تبدیل به فایل‌های JavaScript شوند را مشخص می‌کند.
بخش دوم target است که مراحل Build پروژه را برای این فایل‌های مشخص شده تعیین می‌کند. برای مثال:

همان طور که می‌بینید در قسمت ItemGroup تمام فایل‌های با پسوند ts در پروژه include شده اند. در قسمت target دستور کامپایل این فایل‌ها تعیین شد. اما نکته مهم این است که TypeScript به صورت پیش فرض از ECMAScript 3 در هنگام کامپایل کد‌ها استفاده می‌کند.(ECMAScript 3 در سال 1999 منتشر شد و تقریبا با تمام مرورگرها سازگاری دارد اما از امکانات جدید در Javascript پشتیبانی نمی‌کند). اگر قصد دارید که از ECMAScript 5 در هنگام کامپایل کد‌ها استفاده نمایید کافیست دستور زیر را اضافه نمایید:
--target ES5
مثال:

اما به این نکته دقت داشته باشید که ECMAScript 5 در سال 2009 منتشر شده است در نتیجه فقط با مرورگرهای جدید سازگار خواهد بود و ممکن است کد‌های شما در مرورگرهای قدیمی با مشکل مواجه شود.
مرورگرهایی که از ECMAScript 5 پشتیبانی می‌کنند عبارتند از:
  • IE 9 و نسخه‌های بعد از آن؛
  • FireFox 4 و نسخه‌های بعد از آن؛ 
  • Opera 12 و نسخه‌های بعد از آن؛ 
  • Safari 5.1 و نسخه‌های بعد از آن؛ 
  • Chrome 7 و نسخه‌های بعد از آن.
ادامه دارد...
مطالب
استفاده از افزونه‌های Owin مخصوص سایر نگارش‌های ASP.NET در ASP.NET Core
همانطور که اطلاع دارید یکسری از کتابخانه‌های کمکی و ثالث ASP.NET Core همچون OData و SignalR ، Thinktecture IdentityServer هنوز در حال تکمیل هستند و از آنجایی که هر روزه محبوبیت ASP.NET Core در بین برنامه نویسان در حال افزایش است و خیلی از پروژه‌های نرم افزاری که امروزه start میخورند، از این فریم ورک جدید استفاده میکنند، پس خیلی به اهمیت این مقوله افزوده میشود که بتوان از تکنولوژی‌های فوق در پروژه‌های جدید نیز استفاده کرد و یکی از معقول‌ترین راه‌های ممکن آن، پیاده سازی Owin  در کنار ASP.NET Core Pipeline میباشد. بدین معنا که ما قصد نداریم owin pipeline را جایگزین آن نماییم؛ همانطور که در ادامه‌ی این مقاله گفته خواهد شد، از هر دو معماری (افزونه‌های Owin مخصوص نگارش کامل دات نت و همچنین خود ASP.NET Core) در کنار هم استفاده خواهیم کرد.
پیشنیاز این مقاله آشنایی با ASP.NET Core و MiddleWare و همچنین آشنایی با Owin میباشد. همانطور که عرض کردم اگر مقاله‌ی پیاده سازی OData را مطالعه کرد‌ه‌اید و هم اکنون قصد و یا علاقه‌ی به استفاده‌ی از آن را در پروژه‌ی ASP.NET Core خود دارید، میتوانید این مقاله را دنبال نمایید.
پس تا کنون متوجه شدیم که هدف از این کار، توانایی استفاده از ابزارهایی است که با Owin سازگاری کامل را دارند و همچنین به نسخه‌ی پایدار خود رسیده‌اند. بطور مثال IdentityServer 4 در حال تهیه است که با ASP.NET Core سازگار است. اما هنوز چند نسخه‌ی beta از آن بیرون آمده و به پایداری کامل نرسیده است. همچنین زمان توزیع نهایی آن نیز دقیقا مشخص نیست.
شما برای استفاده از ASP.NET Core RTM 1.0 احتیاج به Visual studio 2015 update 3 دارید. ابتدا یک پروژه‌ی #C از نوع Asp.Net Core Web Application  را «به همراه full .Net Framework» به نام OwinCore میسازیم.
در حال حاضر بدلیل اینکه پکیج‌هایی مانند OData, SignalR هنوز به نسخه‌ی Net Coreی خود آماده نشده‌اند (در حال پیاده سازی هستند)، پس مجبور به استفاده از full .Net framework هستیم و خوب مسلما برنامه در این حالت چند سکویی نخواهد بود. اما به محض اینکه آن‌ها آماده شدند، میتوان full .Net را کنار گذاشته و از NET Core. استفاده نمود و از آنجایی که Owin فقط یک استاندارد هست، هیچ مشکلی با NET Core. نداشته و برنامه را میتوان بدان منتقل کرد و از مزایای چند سکویی بودن آن نیز بهره برد. پس مشکل در حال حاضر Owin نبوده و مجبور به منتظر بودن برای آماده شدن این پکیج‌ها خواهیم بود. مثال عملی این قضیه نیز Nancy است که نسخه‌ی NET Core.ی آن با استفاده از Owin در ASP.NET Core پیاده سازی شده است. در اینجا مثال عملی آن را میتوانید پیدا کنید.


در قسمت بعد، قالب را هم از نوع empty انتخاب مینماییم.


در ادامه فایل project.json را باز کرده و در قسمت dependencies، تغییرات زیر را اعمال نمایید.

قبل از اینکه شما را از این همه وابستگی نگران کنم، باید عرض کنم فقط Microsoft.Owin , Microsoft.AspNetCore.Owin، پکیج‌های اجباری هستند؛ باقی آن‌ها برای نشان دادن انعطاف پذیری بالای این روش میباشند:

"dependencies": {
    "Microsoft.AspNet.OData": "5.9.1",
    "Microsoft.AspNet.SignalR": "2.2.1",
    "Microsoft.AspNet.WebApi.Client": "5.2.3",
    "Microsoft.AspNet.WebApi.Core": "5.2.3",
    "Microsoft.AspNet.WebApi.Owin": "5.2.3",
    "Microsoft.AspNetCore.Diagnostics": "1.0.0",
    "Microsoft.AspNetCore.Hosting": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Owin": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Net.Http": "2.2.29",
    "Microsoft.Owin": "3.0.1",
    "Microsoft.Owin.Diagnostics": "3.0.1",
    "Microsoft.Owin.FileSystems": "3.0.1",
    "Microsoft.Owin.StaticFiles": "3.0.1",
    "Newtonsoft.Json": "9.0.1"
  },
//etc...

بعد از ذخیره کردن این فایل، در پنجره‌ی Output خود شاهد دانلود شدن این پکیج‌ها خواهید بود. در اینجا پکیج‌های مربوط به Owin, Odata, SignalR را مشاهد می‌کنید. ضمن اینکه در کنار آن، AspNetCore.Mvc را نیز مشاهده میفرمایید. دلیل این کار این است که این دو نوع متفاوت قرار است در کنار هم کار کنند و هیچ مشکلی با دیگری ندارند.

در مسیر اصلی پروژه‌ی خود کلاسی به نام OwinExtensions را با محتوای زیر بسازید:

namespace OwinCore
{
    public static class OwinExtensions
    {
        public static IApplicationBuilder UseOwinApp(
            this IApplicationBuilder aspNetCoreApp,
            Action<IAppBuilder> configuration)
        {
            return aspNetCoreApp.UseOwin(setup => setup(next =>
            {
                AppBuilder owinAppBuilder = new AppBuilder();

                IApplicationLifetime aspNetCoreLifetime = (IApplicationLifetime)aspNetCoreApp.ApplicationServices.GetService(typeof(IApplicationLifetime));

                AppProperties owinAppProperties = new AppProperties(owinAppBuilder.Properties);

                owinAppProperties.OnAppDisposing = aspNetCoreLifetime?.ApplicationStopping ?? CancellationToken.None;

                owinAppProperties.DefaultApp = next;

                configuration(owinAppBuilder);

                return owinAppBuilder.Build<Func<IDictionary<string, object>, Task>>();
            }));
        }
    }
}

یک Extension Method به نام UseOwinApp اضافه شده به IApplicationBuilder که مربوط به ASP.NET Core میباشد و درون آن نیز AppBuilder را که مربوط به Owin pipeline میباشد، نمونه سازی کرده‌ایم که باعث میشود Owin pipeline بر روی ASP.NET Core pipeline سوار شود.

حال میخواهیم یک Middleware سفارشی را با استفاده از Owin نوشته و در Startup پروژه، آن را فراخوانی نماییم. کلاسی به نام AddSampleHeaderToResponseHeadersOwinMiddleware را با محتوای زیر تولید مینماییم:

namespace OwinCore
{
    public class AddSampleHeaderToResponseHeadersOwinMiddleware : OwinMiddleware
    {
        public AddSampleHeaderToResponseHeadersOwinMiddleware(OwinMiddleware next)
            : base(next)
        {
        }
        public async override Task Invoke(IOwinContext context)
        {
            //throw new InvalidOperationException("ErrorTest");

            context.Response.Headers.Add("Test", new[] { context.Request.Uri.ToString() });

            await Next.Invoke(context);
        }
    }
}

کلاسی است که از owinMiddleware ارث بری کرده و در متد override شده‌ی Invoke نیز با استفاده از IOwinContext، به پیاده سازی Middleware خود میپردازیم. Exception مربوطه را comment کرده (بعدا در مرحله‌ی تست از آن نیز استفاده مینماییم) و در خط بعدی در هدر response هر request، یک شیء را به نام Test و با مقدار Uri آن request، میسازیم.

خط بعدی هم اعلام میدارد که به Middleware بعدی برود.

در ادامه فایل Startup.cs را باز کرده و اینگونه متد Configure را تغییر دهید:

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }

                owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });
        }

مشاهده میفرمایید با استفاده از UserOwinApp میتوانیم Middleware‌های Owinی خود را register نماییم و نکته‌ی قابل توجه این است که در کنار آن نیز می‌توانیم از IHostingEnviroment مربوط به ASP.NET core استفاده نماییم. owinApp.UseErrorPage از Microsoft.Owin.Diagnostics گرفته شده است و در خط بعدی نیز Middleware شخصی خود را register کرده‌ایم. پروژه را run کرده و در response این را مشاهد مینمایید.

اکنون اگر در Middleware سفارشی خود، آن Exception را از حالت comment در بیاوریم، در صورتیکه در حالت development باشیم، با این صفحه مواجه خواهیم شد:

Exception مربوطه را به حالت comment گذاشته و ادامه میدهیم.

برای اینکه نشان دهیم Owin و ASP.NET Core pipeline در کنار هم میتوانند کار کنند، یک Middleware را از نوع ASP.NET Core نوشته و آن را register مینماییم. کلاسی جدیدی را به نام AddSampleHeaderToResponseHeadersAspNetCoreMiddlware با محتوای زیر میسازیم:

namespace OwinCore
{
    public class AddSampleHeaderToResponseHeadersAspNetCoreMiddlware
    {
        private readonly RequestDelegate Next;

        public AddSampleHeaderToResponseHeadersAspNetCoreMiddlware(RequestDelegate next)
        {
            Next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            //throw new InvalidOperationException("ErrorTest");

            context.Response.Headers.Add("Test2", new[] { "some text" });

            await Next.Invoke(context);
        }
    }
}

متد Configure در Startup.cs را نیز اینگونه تغییر میدهیم

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }

                owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });

            aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>();
        }

اکنون AddSampleHeaderToResponseHeadersAspNetCoreMiddlware رجیستر شده است و بعد از run کردن پروژه و بررسی header response باید این را ببینیم

میبینید که به ترتیب اجرای Middleware‌ها، ابتدا Test مربوط به Owin و بعد آن Test2 مربوط به ASP.NET Core تولید شده است.

حال اجازه دهید Odata را با استفاده از Owin پیاده سازی نماییم. ابتدا کلاسی را به نام Product با محتوای زیر تولید نمایید:

namespace OwinCore
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

حال کلاسی را به نام ProductsController با محتوای زیر میسازیم:

namespace OwinCore
{
    public class ProductsController : ODataController
    {
        [EnableQuery]
        public IQueryable<Product> Get()
        {
            return new List<Product>
            {
                 new Product { Id = 1, Name = "Test" , Price = 10 }
            }
            .AsQueryable();
        }
    }
}

اگر مقاله‌ی پیاده سازی Crud با استفاده از OData را مطالعه کرده باشید، قاعدتا با این کد‌ها آشنا خواهید بود. ضمن اینکه پرواضح است که OData هیچ وابستگی به entity framework ندارد.

برای config آن نیز در Startup.cs پروژه و متد Configure، تغییرات زیر را اعمال مینماییم.

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            //aspNetCoreApp.UseMvc();

            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }
                // owinApp.UseFileServer(); as like as asp.net core static files middleware
                // owinApp.UseStaticFiles(); as like as asp.net core static files middleware
                // owinApp.UseWebApi(); asp.net web api / odata / web hooks

                HttpConfiguration webApiConfig = new HttpConfiguration();
                ODataModelBuilder odataMetadataBuilder = new ODataConventionModelBuilder();
                odataMetadataBuilder.EntitySet<Product>("Products");
                webApiConfig.MapODataServiceRoute(
                    routeName: "ODataRoute",
                    routePrefix: "odata",
                    model: odataMetadataBuilder.GetEdmModel());
                owinApp.UseWebApi(webApiConfig);

                owinApp.MapSignalR();

                //owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });

            //aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>();
        }

برای config مخصوص Odata، به HttpConfiguration نیاز داریم. بنابراین instanceی از آن گرفته و برای مسیریابی Odata از آن استفاده مینماییم.

با استفاده از پیاده سازی که از استاندارد Owin انجام دادیم، مشاهده کردید که Odata را همانند یک پروژه‌ی معمولی asp.netی، config نمودیم. در خط بعدی هم SignalR را مشاهده مینمایید.

اکنون اگر آدرس زیر را در مرورگر خود وارد نمایید، پاسخ زیر را از Odata دریافت خواهید کرد:

http://localhost:YourPort/odata/Products

بعد از فرستادن request فوق، باید response زیر را دریافت نمایید:

{ 
 "@odata.context":"http://localhost:4675/odata/$metadata#Products","value":[
    {
      "Id":1,"Name":"Test","Price":10
    }
  ]
}

تعداد زیادی Owin Middleware موجود همانند Thinktecture IdentityServer, NWebSec, Nancy, Facebook OAuth , ... هم با همان آموزش راه اندازی بر روی Owin که دارند میتوانند در ASP.NET Core نیز استفاده شوند و زمانی که نسخه‌ی ASP.NET Core اینها به آمادگی کامل رسید، با کمترین تغییری میتوان از آنها استفاده نمود.
اگر در پیاده سازی مقاله‌ی فوق با مشکل رو به رو شدید (اگر مراحل به ترتیب اجرا شوند، مشکلی نخواهید داشت) میتوانید از Github ، کل این پروژه را clone کرده و همچنین طبق commit‌های منظم از طریق history آن، مراحل جلو رفتن پروژه را دنبال نمایید.
مطالب
ارسال ویدیو بصورت Async توسط Web Api
فریم ورک ASP.NET Web API صرفا برای ساخت سرویس‌های ساده‌ای که می‌شناسیم، نیست و در واقع مدل جدیدی برای برنامه نویسی HTTP است. کارهای بسیار زیادی را می‌توان توسط این فریم ورک انجام داد که در این مقاله به یکی از آنها می‌پردازم. فرض کنید می‌خواهیم یک فایل ویدیو را بصورت Asynchronous به کلاینت ارسال کنیم.

ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب آن را MVC + Web API انتخاب کنید.


ابتدا به فایل WebApiConfig.cs در پوشه App_Start مراجعه کنید و مسیر پیش فرض را حذف کنید. برای مسیریابی سرویس‌ها از قابلیت جدید Attribute Routing استفاده خواهیم کرد. فایل مذکور باید مانند لیست زیر باشد.
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();
    }
}
حال در مسیر ریشه پروژه، پوشه جدیدی با نام Videos ایجاد کنید و یک فایل ویدیو نمونه بنام sample.mp4 در آن کپی کنید. دقت کنید که فرمت فایل ویدیو در مثال جاری mp4 در نظر گرفته شده اما به سادگی می‌توانید آن را تغییر دهید.
سپس در پوشه Models کلاس جدیدی بنام VideoStream ایجاد کنید. این کلاس مسئول نوشتن داده فایل‌های ویدیویی در OutputStream خواهد بود. کد کامل این کلاس را در لیست زیر مشاهده می‌کنید.
public class VideoStream
{
    private readonly string _filename;
    private long _contentLength;

    public long FileLength
    {
        get { return _contentLength; }
    }

    public VideoStream(string videoPath)
    {
        _filename = videoPath;
        using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            _contentLength = video.Length;
        }
    }

    public async void WriteToStream(Stream outputStream,
        HttpContent content, TransportContext context)
    {
        try
        {
            var buffer = new byte[65536];

            using (var video = File.Open(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                var length = (int)video.Length;
                var bytesRead = 1;

                while (length > 0 && bytesRead > 0)
                {
                    bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length));
                    await outputStream.WriteAsync(buffer, 0, bytesRead);
                    length -= bytesRead;
                }
            }
        }
        catch (HttpException)
        {
            return;
        }
        finally
        {
            outputStream.Close();
        }
    }
}

شرح کلاس VideoStream
این کلاس ابتدا دو فیلد خصوصی تعریف می‌کند. یکی filename_ که فقط-خواندنی است و نام فایل ویدیو درخواستی را نگهداری می‌کند. و دیگری contentLength_ که سایز فایل ویدیو درخواستی را نگهداری می‌کند.

یک خاصیت عمومی بنام FileLength نیز تعریف شده که مقدار خاصیت contentLength_ را بر می‌گرداند.

متد سازنده این کلاس پارامتری از نوع رشته بنام videoPath را می‌پذیرد که مسیر کامل فایل ویدیوی مورد نظر است. در این متد، متغیر‌های filename_ و contentLength_ مقدار دهی می‌شوند. نکته‌ی قابل توجه در این متد استفاده از پارامتر FileShare.Read است که باعث می‌شود فایل مورد نظر هنگام باز شدن قفل نشود و برای پروسه‌های دیگر قابل دسترسی باشد.

در آخر متد WriteToStream را داریم که مسئول نوشتن داده فایل‌ها به OutputStream است. اول از همه دقت کنید که این متد از کلمه کلیدی async استفاده می‌کند بنابراین بصورت asynchronous اجرا خواهد شد. در بدنه این متد متغیری بنام buffer داریم که یک آرایه بایت با سایز 64KB را تعریف می‌کند. به بیان دیگر اطلاعات فایل‌ها را در پکیج‌های 64 کیلوبایتی برای کلاینت ارسال خواهیم کرد. در ادامه فایل مورد نظر را باز می‌کنیم (مجددا با استفاده از FileShare.Read) و شروع به خواندن اطلاعات آن می‌کنیم. هر 64 کیلوبایت خوانده شده بصورت async در جریان خروجی نوشته می‌شود و تا هنگامی که به آخر فایل نرسیده ایم این روند ادامه پیدا می‌کند.
while (length > 0 && bytesRead > 0)
{
    bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length));
    await outputStream.WriteAsync(buffer, 0, bytesRead);
    length -= bytesRead;
}
اگر دقت کنید تمام کد بدنه این متد در یک بلاک try/catch قرار گرفته است. در صورتی که با خطایی از نوع HttpException مواجه شویم (مثلا هنگام قطع شدن کاربر) عملیات متوقف می‌شود و در آخر نیز جریان خروجی (outputStream) بسته خواهد شد. نکته دیگری که باید بدان اشاره کرد این است که کاربر حتی پس از قطع شدن از سرور می‌تواند ویدیو را تا جایی که دریافت کرده مشاهده کند. مثلا ممکن است 10 پکیج از اطلاعات را دریافت کرده باشد و هنگام مشاهده پکیج دوم از سرور قطع شود. در این صورت امکان مشاهده ویدیو تا انتهای پکیج دهم وجود خواهد داشت.

حال که کلاس VideoStream را در اختیار داریم می‌توانیم پروژه را تکمیل کنیم. در پوشه کنترلر‌ها کلاسی بنام VideoControllerبسازید. کد کامل این کلاس را در لیست زیر مشاهده می‌کنید.
public class VideoController : ApiController
{
    [Route("api/video/{ext}/{fileName}")]
    public HttpResponseMessage Get(string ext, string fileName)
    {
        string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
        if (File.Exists(videoPath))
        {
            FileInfo fi = new FileInfo(videoPath);
            var video = new VideoStream(videoPath);

            var response = Request.CreateResponse();

            response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream,
                new MediaTypeHeaderValue("video/" + ext));

            response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fi.Name.Replace(" ", ""));
            response.Content.Headers.Add("Content-Length", video.FileLength.ToString());

            return response;
        }
        else
        {
            return Request.CreateResponse(HttpStatusCode.NotFound);
        }
    }
}

شرح کلاس VideoController
همانطور که می‌بینید مسیر دستیابی به این کنترلر با استفاده از قابلیت Attribute Routing تعریف شده است.

[Route("api/video/{ext}/{fileName}")]
نمونه ای از یک درخواست که به این مسیر نگاشت می‌شود:
api/video/mp4/sample
بنابراین این مسیر فرمت و نام فایل مورد نظر را بدین شکل می‌پذیرد. در نمونه جاری ما فایل sample.mp4 را درخواست کرده ایم.
متد Get این کنترلر دو پارامتر با نام‌های ext و fileName را می‌پذیرد که همان فرمت و نام فایل هستند. سپس با استفاده از کلاس HostingEnvironment سعی می‌کنیم مسیر کامل فایل درخواست شده را بدست آوریم.
string videoPath = HostingEnvironment.MapPath(string.Format("~/Videos/{0}.{1}", fileName, ext));
استفاده از این کلاس با Server.MapPath تفاوتی نمی‌کند. در واقع خود Server.MapPath نهایتا همین کلاس HostingEnvironment را فراخوانی می‌کند. اما در کنترلر‌های Web Api به کلاس Server دسترسی نداریم. همانطور که مشاهده می‌کنید فایل مورد نظر در پوشه Videos جستجو می‌شود، که در ریشه سایت هم قرار دارد. در ادامه اگر فایل درخواست شده وجود داشت وهله جدیدی از کلاس VideoStream می‌سازیم و مسیر کامل فایل را به آن پاس می‌دهیم.
var video = new VideoStream(videoPath);
سپس آبجکت پاسخ را وهله سازی می‌کنیم و با استفاده از کلاس PushStreamContent اطلاعات را به کلاینت می‌فرستیم.
var response = Request.CreateResponse();

response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/" + ext));

کلاس PushStreamContent در فضای نام System.Net.Http وجود دارد. همانطور که می‌بینید امضای Action پاس داده شده، با امضای متد WriteToStream در کلاس VideoStream مطابقت دارد.

در آخر دو Header به پاسخ ارسالی اضافه می‌کنیم تا نوع داده ارسالی و سایز آن را مشخص کنیم.
response.Content.Headers.Add("Content-Disposition", "attachment;filename=" + fileName);
response.Content.Headers.Add("Content-Length", video.FileLength.ToString());
افزودن این دو مقدار مهم است. در صورتی که این Header‌‌ها را تعریف نکنید سایز فایل دریافتی و مدت زمان آن نامعلوم خواهد بود که تجربه کاربری خوبی بدست نمی‌دهد. نهایتا هم آبجکت پاسخ را به کلاینت ارسال می‌کنیم. در صورتی هم که فایل مورد نظر در پوشه Videos پیدا نشود پاسخ NotFound را بر می‌گردانیم.
if(File.Exists(videoPath))
{
    // removed for bravity
}
else
{
    return Request.CreateResponse(HttpStatusCode.NotFound);
}
خوب، برای تست این مکانیزم نیاز به یک کنترلر MVC و یک View داریم. در پوشه کنترلر‌ها کلاسی بنام HomeController ایجاد کنید که با لیست زیر مطابقت داشته باشد.
public class HomeController : Controller
{
    // GET: Home
    public ActionResult Index()
    {
        return View();
    }
}
نمای این متد را بسازید (با کلیک راست روی متد Index و انتخاب گزینه Add View) و کد آن را مطابق لیست زیر تکمیل کنید.
<div>
    <div>
        <video width="480" height="270" controls="controls" preload="auto">
            <source src="/api/video/mp4/sample" type="video/mp4" />
            Your browser does not support the video tag.
        </video>
    </div>
</div>
همانطور که مشاهده می‌کنید یک المنت ویدیو تعریف کرده ایم که خواص طول، عرض و غیره آن نیز مقدار دهی شده اند. زیر تگ source متنی درج شده که در صورت لزوم به کاربر نشان داده می‌شود. گرچه اکثر مرورگرهای مدرن از المنت ویدیو پشتیبانی می‌کنند. تگ سورس فایلی با مشخصات sample.mp4 را درخواست می‌کند و نوع آن را نیز video/mp4 مشخص کرده ایم.

اگر پروژه را اجرا کنید می‌بینید که ویدیو مورد نظر آماده پخش است. برای اینکه ببینید چطور داده‌های ویدیو در قالب پکیج‌های 64 کیلو بایتی دریافت می‌شوند از ابزار مرورگرتان استفاده کنید. مثلا در گوگل کروم F12 را بزنید و به قسمت Network بروید. صفحه را یکبار مجددا بارگذاری کنید تا ارتباطات شبکه مانیتور شود. اگر به المنت sample دقت کنید می‌بینید که با شروع پخش ویدیو پکیج‌های اطلاعات یکی پس از دیگری دریافت می‌شوند و اطلاعات ریز آن را می‌توانید مشاهده کنید.

پروژه نمونه به این مقاله ضمیمه شده است. قابلیت Package Restore فعال شده و برای صرفه جویی در حجم فایل، تمام پکیج‌ها و محتویات پوشه bin حذف شده اند. برای تست بیشتر می‌توانید فایل sample.mp4 را با فایلی حجیم‌تر جایگزین کنید تا نحوه دریافت اطلاعات را با روشی که در بالا بدان اشاره شد مشاهده کنید.

AsyncVideoStreaming.rar