مطالب
توسعه برنامه های 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‌) نیز در ادامه بحث خواهند شد.
مطالب
طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید

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


یک مثال واقعی

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

ایجاد یک قرارداد برای موجودیت‌های دارای شماره منحصر به فرد
public interface INumberedEntity
{
    string Number { get; set; }
}
با استفاده از این واسط می‌توان از تکرار یکسری از تنظیمات مانند تنظیم طول فیلد Number و همچنین ایجاد ایندکس منحصر به فرد برروی آن، به شکل زیر جلوگیری کرد.
foreach (var entityType in builder.Model.GetEntityTypes()
    .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType)))
{
    builder.Entity(entityType.ClrType)
        .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50);

    if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType))
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId))
            .HasName(
                $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
    else
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number))
            .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
}

ایجاد یک Entity برای نگهداری شماره قابل استفاده بعدی مرتبط با موجودیت‌ها
public class NumberedEntity : Entity, IMultiTenantEntity
{
    public string EntityName { get; set; }
    public long NextNumber { get; set; }
    
    public long TenantId { get; set; }
}

با تنظیمات زیر:
public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity>
{
    public void Configure(EntityTypeBuilder<NumberedEntity> builder)
    {
        builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false);
        builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique();
        builder.ToTable(nameof(NumberedEntity));
    }
}

شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت می‌توان آخرین شماره ثبت شده‌ی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:

  • ایجاد Gap مابین شماره‌های تولید شده، که مدنظر ما نمی‌باشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شماره‌ای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)


پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number

internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity>
{
    private readonly IUnitOfWork _uow;
    private readonly IOptions<NumberingConfiguration> _configuration;

    public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));
        _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
    }

    protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata)
    {
        if (!entity.Number.IsNullOrEmpty()) return;

        bool retry;
        string nextNumber;
        
        do
        {
            nextNumber = GenerateNumber(entity);
            var exists = CheckDuplicateNumber(entity, nextNumber);
            retry = exists;
            
        } while (retry);
        
        entity.Number = nextNumber;
    }

    private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber)
    {
       //...
    }

    private string GenerateNumber(INumberedEntity entity)
    {
       //...
    }
}

ابتدا بررسی می‌شود اگر پراپرتی Number مقداردهی شده‌است، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شماره‌ای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت می‌گیرد.


بررسی متد GenerateNumber

private string GenerateNumber(INumberedEntity entity)
{
    var option = _configuration.Value.NumberedEntityOptions[entity.GetType()];

    var entityName = $"{entity.GetType().FullName}";

    var lockKey = $"Tenant_{_uow.TenantId}_" + entityName;

    _uow.ObtainApplicationLevelDatabaseLock(lockKey);

    var nextNumber = option.Start.ToString();

    var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName);
    if (numberedEntity == null)
    {
        _uow.ExecuteSqlCommand(
            "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName,
            option.Start + option.IncrementBy, _uow.TenantId);
    }
    else
    {
        nextNumber = numberedEntity.NextNumber.ToString();
        _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ",
            numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id);
    }

    if (!string.IsNullOrEmpty(option.Prefix))
        nextNumber = option.Prefix + nextNumber;
    
    return nextNumber;
}

ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد می‌کنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنش‌های همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید می‌باشد، با توجه به اینکه INumberedEntity می‌باشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.

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

public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource)
{
    uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, 
                @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000);
}

با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیت‌های INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونه‌ها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی می‌شود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود. 

گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری می‌باشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام می‌گیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.

ساختار NumberingConfiguration

public class NumberingConfiguration
{
    public bool Enabled { get; set; }

    public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } =
        new Dictionary<Type, NumberedEntityOption>();
}
public class NumberedEntityOption
{
    public string Prefix { get; set; }
    public int Start { get; set; } = 1;
    public int IncrementBy { get; set; } = 1;
}

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

گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیت‌ها

public static void AddNumbering(this IServiceCollection services,
    IDictionary<Type, NumberedEntityOption> options)
{
    services.Configure<NumberingConfiguration>(configuration =>
    {
        configuration.Enabled = true;
        configuration.NumberedEntityOptions.AddRange(options);
    });
    
    services.AddTransient<IPreActionHook, NumberingPreInsertHook>();
}

و استفاده از این متد الحاقی در Startup پروژه

services.AddNumbering(new Dictionary<Type, NumberedEntityOption>
{
    [typeof(Task)] = new NumberedEntityOption
    {
        Prefix = "T_",
        Start = 1000,
        IncrementBy = 5
    }
});

و موجودیت Task

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

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

با خروجی‌های زیر

پ.ن ۱: در برخی از Domain‌ها نیاز به ریست کردن این شماره‌ها براساس یکسری فیلد موجود در موجودیت مورد نظر نیز مطرح می‌باشد. به عنوان مثال در یک سیستم انبارداری شاید براساس FiscalYear و در یک سیستم فروش با توجه به نحوه فروش (SaleType)، لازم باشد این ریست برای شماره‌های موجودیت «سفارش»، انجام پذیرد. در کل با کمی تغییرات می‌توان از این روش مطرح شده در چنین حالاتی نیز به عنوان یک ابزار شماره گذاری خودکار کمک گرفت.
پ.ن ۲: استفاده از امکانات  Sequence در Sql Server هم شاید اولین راه حلی باشد که به ذهن می‌رسد؛ ولی از آنجایی که از تراکنش‌ها پشتیبانی ندارد، مسئله Gap بین شماره‌ها پابرجاست و همچنین آزادی عملی را به این شکل که در مطلب مطرح شد، نداریم.
مطالب
شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه
در ادامه‌ی سری «ارتقاء به ASP.NET Core 1.0» اگر بخواهیم مباحث اعتبارسنجی کاربران و ASP.NET Identity مخصوص آن‌را بررسی کنیم، نیاز است ابتدا مباحث Entity framework Core 1.0 را بررسی کنیم. به همین جهت در طی چند قسمت مباحث پایه‌ای کار با EF Core 1.0 را در ASP.NET Core 1.0، بررسی خواهیم کرد. بنابراین پیشنیاز ضروری این مباحث، مطالعه‌ی سری «ارتقاء به ASP.NET Core 1.0» است و در آن از مباحثی مانند چگونگی کار با فایل‌های کانفیگ جدید، تزریق وابستگی‌ها و سرویس‌ها، فعال سازی سرویس Logging، فعال سازی صفحات مخصوص توسعه دهنده‌ها و ... در ASP.NET Core 1.0 استفاده خواهد شد.


EF Core چیست؟

EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانک‌های اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر می‌کند. توسط آن قسمت عمده‌ی کدهای مستقیم کار با بانک‌های اطلاعاتی حذف شده و تبدیل به کدهای دات نتی می‌شوند. مزیت این لایه‌ی Abstraction اضافی (لایه‌ای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامه‌است؛ بدون نیاز به تغییری در سایر قسمت‌های برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر می‌کنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر می‌رسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئری‌ها، رفتار بسیاری از بانک‌های اطلاعاتی با آن‌ها همانند رویه‌های ذخیره شده است که به عدم تولید Query plan‌های مجزایی به ازای هر کوئری رسیده منجر می‌شود که در نهایت سبب بالا رفتن سرعت اجرای کوئری‌ها و مصرف حافظه‌ی کمتری در سمت سرور بانک اطلاعاتی می‌گردد.


تفاوت EF Core با نگارش‌های دیگر Entity framework در چیست؟

سورس باز بودن
EF از نگارش‌های آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارش‌های پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.

چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفاده‌ی از آن در انواع و اقسام برنامه‌های دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامه‌های مختلف به شرح زیر است:
 • All .NET application (Console, ASP.NET 4, WinForms, WPF)
 • Mac and Linux applications (Mono)
 • UWP (Universal Windows Platform)
 • ASP.NET Core applications
 • Can use EF Core in Windows phone and Windows store app

افزایش تعداد بانک‌های اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانک‌های اطلاعاتی رابطه‌‌ای بود و همچنین مایکروسافت صرفا نگارش‌های مختلف SQL Server را به صورت رسمی پشتیبانی می‌کرد و برای سایر بانک‌های اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانک‌های اطلاعاتی رابطه‌ای، پشتیبانی از بانک‌های اطلاعاتی NoSQL هم اضافه شده‌است؛ به همراه پروایدر ویژه‌‌ای به نام In Memory جهت انجام ساده‌تر Unit testing. کاری که با نگارش‌های پیشین EF به سادگی و از روز اول پشتیبانی نمی‌شد.

حذف و یا عدم پیاده سازی تعدادی از قابلیت‌های EF 6.x
اگر موارد فوق جزو مهم‌ترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگی‌ها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامه‌های UWP نیاز دارد تمام نوع‌ها را همانند زبان‌های C و ++C، در زمان کامپایل بداند)، یک سری از قابلیت‌های EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمی‌شوند. لیست کامل و به روز شده‌ی این موارد را در اینجا می‌توانید مطالعه کنید.
بنابراین امکان انتقال برنامه‌های EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.


برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0

برای نصب تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بسته‌ی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
البته در این قسمت صرفا از بسته‌ی اول که جهت اتصال به SQL Server است استفاده می‌کنیم. بسته‌های دیگر را در قسمت‌های بعد، برای به روز رسانی اسکیمای بانک اطلاعاتی (مباحث Migrations) و مباحث scaffolding استفاده خواهیم کرد.
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
        "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0"
    }
}
این مداخلی که توسط نیوگت اضافه شده‌اند، نیاز به اصلاح دارند؛ به این صورت:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "type": "build"
        },
        "Microsoft.EntityFrameworkCore.SqlServer.Design": {
            "version": "1.0.0",
            "type": "build"
        }
    },

    "tools": {
       // same as before
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "imports": [
                "portable-net45+win8"
            ]
        }   
   }
}
نیاز است در قسمت dependencies مشخص کنیم که ابزارهای اضافه شده مخصوص build هستند و نه اجرای برنامه. همچنین قسمت tools را باید با Microsoft.EntityFrameworkCore.Tools مقدار دهی کرد تا بتوان از این ابزار در خط فرمان، جهت اجرای فرامین migrations استفاده کرد.
بنابراین از همین ابتدای کار، بدون مراجعه‌ی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آن‌را ذخیره کنید؛ تا کار به روز رسانی و نصب بسته‌ها، به صورت خودکار و همچنین صحیحی انجام شود.


فعال سازی صفحات مخصوص توسعه دهنده‌های EF Core 1.0

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بسته‌ی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحه‌ی نمایش خطاهای بانک اطلاعاتی را میسر می‌کند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{
    "dependencies": {
       // same as before
        "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0"
    }
}
سپس می‌توان متد جدید UseDatabaseErrorPage را در متد Configure کلاس آغازین برنامه، فراخوانی کرد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   if (env.IsDevelopment())
   {
      app.UseDatabaseErrorPage();
   }
با فعال سازی این صفحه، اگر در حین توسعه‌ی برنامه و اتصال به بانک اطلاعاتی، خطایی رخ دهد، بجای مشاهده‌ی یک صفحه‌ی خطای عمومی (اگر UseDeveloperExceptionPage را فعال کرده باشید)، اینبار ریز جزئیات بیشتری را به همراه توصیه‌هایی از طرف تیم EF مشاهده خواهید کرد.


تعریف اولین Context برنامه و مشخص سازی رشته‌ی اتصالی آن


در این تصویر، زیر ساخت نگاشت‌های EF Core را مشاهده می‌کنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرنده‌ی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل این‌ها را مشاهده می‌کنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که این‌ها نیز به فیلدها و ستون‌های آن جدول در سمت بانک اطلاعاتی نگاشت می‌شوند.
بنابراین برای شروع کار، پوشه‌ای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه می‌کنیم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
کلاس Person بیانگر ساختار جدول اشخاص بانک اطلاعاتی است. برای اینکه این کلاس را تبدیل و نگاشت به یک جدول کنیم، نیاز است آن‌را به صورت یک DbSet در معرض دید EF Core قرار دهیم و اینکار در کلاسی که از DbContex مشتق می‌شود، صورت خواهد گرفت:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Person> Persons { get; set; }
    }
}
بنابراین در ادامه کلاس جدید ApplicationDbContext را که از کلاس پایه DbContext مشتق می‌شود تعریف کرده و سپس کلاس Person را به صورت یک DbSet در معرض دید EF Core قرار می‌دهیم.
سازنده‌ی این کلاس نیز به نحو خاصی تعریف شده‌است. اگر به سورس‌های EF Core مراجعه کنیم، کلاس پایه‌ی DbContext دارای دو سازنده‌ی با و بدون پارامتر است:
protected DbContext()
   : this((DbContextOptions) new DbContextOptions<DbContext>())
{
}

public DbContext([NotNull] DbContextOptions options)
{
  // …
}
اگر از سازنده‌ی بدون پارامتر استفاده کنیم و برای مثال در کلاس ApplicationDbContext فوق، به طور کامل سازنده‌ی تعریف شده را حذف کنیم، باید به نحو ذیل تنظیمات بانک اطلاعاتی را مشخص کنیم:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"... connection string ...");
        }
    }
}
در این حالت باید متد OnConfiguring را override و یا بازنویسی کنیم، تا بتوان از اول مشخص کرد که قرار است از پروایدر SQL Server استفاده کنیم و ثانیا رشته‌ی اتصالی به آن چیست.
اما چون در یک برنامه‌ی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام می‌شود و در آنجا به سادگی می‌توان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشته‌ی اتصالی را دریافت کرد، مرسوم است از سازنده‌ی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعه‌ی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» به فایل appsettings.json مراجعه کرده و تنظیمات رشته‌ی اتصالی برنامه را به صورت ذیل در آن مشخص می‌کنیم:
{
    "ConnectionStrings": {
        "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true"
    }
}
باید دقت داشت که نام این مداخل کاملا اختیاری هستند و در نهایت باید در کلاس آغازین برنامه به صورت صریحی مشخص شوند.
در اینجا به وهله‌ی پیش فرض SQL Server اشاره شده‌است؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده می‌شود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیده‌است.

پس از تعریف رشته‌ی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازنده‌ی با پارامتر آن استفاده می‌کنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویس‌های ASP.NET Core معرفی می‌کنیم:
    public class Startup
    {
        public IConfigurationRoot Configuration { set; get; }

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                                .SetBasePath(env.ContentRootPath)
                                .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                                .AddJsonFile($"appsettings.{env}.json", optional: true);
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]);
            });
در اینجا جهت یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» نحوه‌ی وهله سازی خاصیت Configuration که در متد UseSqlServer مورد استفاده قرار گرفته‌است، نیز ذکر شده‌است.
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی می‌کنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.


یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد

با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگی‌ها نیز در آن وجود دارد. یعنی بجای روش فوق، می‌توان IConfigurationRoot را به سازنده‌ی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        private readonly IConfigurationRoot _configuration;

        public ApplicationDbContext(IConfigurationRoot configuration)
        {
            _configuration = configuration;
        }

        //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        //{
        //}

        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]);
        }
    }
}
با توجه به اینکه IConfigurationRoot در کلاس ConfigureServices به صورت Singleton، به مجموعه‌ی سرویس‌های برنامه معرفی شده‌است، از آن در تمام کلاس‌های برنامه که تحت نظر سیستم تزریق وابستگی‌های توکار ASP.NET Core هستند، می‌توان استفاده کرد.
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا می‌کند و ساده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>();


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

همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامه‌ی ASP.NET Core نیز وجود دارد:
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.AspNetCore.Mvc;

namespace Core1RtmEmptyTest.Controllers
{
    public class TestDBController : Controller
    {
        private readonly ApplicationDbContext _ctx;

        public TestDBController(ApplicationDbContext ctx)
        {
            _ctx = ctx;
        }

        public IActionResult Index()
        {
            var name = _ctx.Persons.First().FirstName;
            return Json(new { firstName = name });
        }
    }
}
در اینجا نحوه‌ی تزریق DB Context برنامه را به یک کنترلر مشاهده می‌کنید. البته هرچند تزریق یک کلاس مشخص به این شکل، تزریق وابستگی‌ها نام ندارد و هنوز این کنترلر دقیقا وابسته‌است به پیاده سازی خاص کلاس ApplicationDbContext، اما ... در کل امکان آن هست.

در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:


علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشده‌است و همچنین ساختار جداول را به آن منتقل نکرده‌ایم که این موارد را در قسمت‌های بعدی مرور خواهیم کرد.
نظرات مطالب
امکان بررسی سلامت برنامه در ASP.NET Core 2.2
جهت رفع مشکل در تنظیمات HealthCheck آدرس به صورت Absolute uri تنظیم شود
  "HealthChecksUI": {
    "HealthChecks": [
      {
        "Name": "Supported Services",
        "Uri": "https://Ip server/uaa/health-services"
      },
      {
        "Name": "Supported Databases",
        "Uri": "https://Ip server/uaa/health-databases"
      }
    ],
    "EvulationTimeInSeconds": 50
  }

نظرات مطالب
پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
ارتقاء به NET Core 3.0.: پشتیبانی از ایجاد سرویس‌های پس‌زمینه

یکی از تغییرات مهم قالب ایجاد پروژه‌های ASP.NET Core 3.0، تغییر فایل program.cs آن است که در آن از یک Generic Host بجای روش قبلی Web Host، استفاده شده‌است. علت آن فراهم آوردن امکان استفاده‌ی از قابلیت‌هایی مانند تزریق وابستگی‌ها، logging، تنظیمات برنامه و غیره، در برنامه‌های غیر وب نیز می‌باشد. یکی از این انواع برنامه‌ها، سرویس‌های پس‌زمینه‌ی غیر HTTP هستند. به این ترتیب می‌توان برنامه‌ای شبیه به یک برنامه‌ی وب ASP.NET Core را ایجاد کرد که تنها کارش اجرای سرویس‌های غیر وبی است؛ اما به تمام امکانات و زیر ساخت‌های ASP.NET Core دسترسی دارد.
برای ایجاد این نوع برنامه‌ها در NET Core 3x. می‌توانید دستور زیر را در پوشه‌ی خالی که ایجاد کرده‌اید، اجرا کنید:
dotnet new worker
ساختار برنامه‌ای که توسط این دستور تولید می‌شود به صورت زیر است که بسیار شبیه به ساختار یک برنامه‌ی ASP.NET Core است:
appsettings.Development.json
appsettings.json
MyWorkerServiceApp.csproj
Program.cs
Worker.cs

- فایل csproj آن دارای این محتوا است:
<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <UserSecretsId>dotnet-MyWorkerServiceApp-B76DB08E-FFBB-4AD1-89B5-93BF483D1BD0</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0-preview8.19405.4" />
  </ItemGroup>
</Project>
در آن ویژگی Sdk به Microsoft.NET.Sdk.Worker اشاره می‌کند و همچنین از بسته‌ی Microsoft.Extensions.Hosting استفاده شده‌است.

- محتوای فایل Program.cs آن بسیار آشنا است و دقیقا کپی همان فایلی است که در برنامه‌های ASP.NET Core 3x حضور دارد:
namespace MyWorkerServiceApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();
                });
    }
}
در اینجا یک Generic host را بجای Web host قالب‌های پیشین فایل Program.cs ملاحظه می‌کنید که هدف اصلی آن، عمومی کردن این قالب، برای استفاده‌ی از آن در برنامه‌های غیر وبی نیز می‌باشد.
در متد ConfigureServices، انواع اقسام سرویس‌ها را منجمله یک HostedService که در مطلب جاری به آن پرداخته شده، می‌توان افزود. سرویس Worker ای که در اینجا به آن ارجاعی وجود دارد، به صورت زیر تعریف شده‌است:
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;

        public Worker(ILogger<Worker> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
با ساختار این کلاس نیز آشنا هستید و موضوع اصلی مطلب جاری است.


یک نکته‌ی تکمیلی: روش تبدیل کردن یک BackgroundService به یک Windows Service

اگر برنامه‌ی NET Core. شما در ویندوز اجرا می‌شود، می‌توانید این برنامه‌ی BackgroundService را به یک سرویس ویندوز NT نیز تبدیل کنید. برای اینکار ابتدا بسته‌ی نیوگت Microsoft.Extensions.Hosting.WindowsServices را به پروژه اضافه کنید. سپس جائیکه CreateHostBuilder صورت می‌گیرد، متد UseWindowsService را فراخوانی کنید:
public static IHostBuilder CreateHostBuilder(string[] args) => 
            Host.CreateDefaultBuilder(args) 
                .UseWindowsService() 
                .ConfigureServices((hostContext, services) => 
                { 
                   //services.AddHttpClient(); 
                   services.AddHostedService<Worker>(); 
                });
تا اینجا هنوز هم برنامه، شبیه به یک برنامه‌ی کنسول دات نت Core قابل اجرا و دیباگ است. اما اگر خواستید آن‌را به صورت یک سرویس ویندوز نیز نصب کنید، تنها کافی است از دستور زیر استفاده کنید:
 cs create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe

البته برای لینوکس نیز می‌توان از UseSystemd استفاده کرد که نیاز به نصب بسته‌ی Microsoft.Extensions.Hosting.Systemd را دارد:
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseSystemd()
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });
مسیرراه‌ها
Entity framework code-first
شروع به کار با EF Code first

برای تکمیل بحث نیاز است تغییرات انجام شده از نگارش 4 به 6 را نیز مد نظر داشته باشید:


آشنایی با مباحث Migrations



آشنایی با تنظیمات نگاشت‌ها به دو روش استفاده از ویژگی‌ها و Fluent API



اعتبارسنجی و بررسی استثناءها



ردیابی تغییرات



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

      نکات مهم کوئری نویسی در EF



      استفاده از EF در WPF


      لایه بندی پروژه‌های EF Code first



      پروژ‌ه‌های انجام شده با EF Code first

       
      اشتراک‌ها
      gRPC در دات نت ۵

      حضور وحید فرهمندیان در برنامه .NET Docs Show شرکت مایکروسافت و صحبت در مورد:

      Build High Performance Services With gRPC and .NET 5

      gRPC در دات نت ۵