پیشنیازهای نصب Docker بر روی ویندوز
مطابق مستندات آن، برای نصب داکر بر روی ویندوز به حداقلهای زیر نیاز است:
- استفاده از ویندوز 10 نگارش enterprise، که شماره نگارش آن حداقل 1607 باشد (حداقل Anniversary Update باشد).
- مجازی سازی در BIOS فعال شده باشد.
البته مجازی سازی عموما به صورت پیشفرض فعال است. برای بررسی آن، taskmanager ویندوز را اجرا کرده و در برگهی Performance آن، جائیکه مشخصات CPU را نمایش میدهد، یک سطر به Virtualization اختصاص دارد که مقدار آن باید enabled باشد (تصویر زیر) و اگر نیست، برای فعال کردن آن باید به تنظیمات BIOS سیستم خود مراجعه کنید:
روش دیگر دریافت این اطلاعات، اجرای دستور systeminfo در خط فرمان، با دسترسی مدیریتی است. در خروجی آن، عبارت «Virtualization Enabled In Firmware» را جستجو کنید که باید مقدار آن yes باشد.
- داشتن CPU با قابلیت SLAT یا Second Level Address Translation.
برای یافتن این موضوع، برنامهی coreinfo را دریافت کرده و آنرا به صورت coreinfo -v اجرا کنید. خروجی آن سه سطر مرتبط با مجازی سازی را به همراه دارد. اگر قابلیتی موجود نباشد، جلوی آن یک خط تیره و اگر قابلیتی موجود باشد، روبروی آن یک ستاره را مشاهده خواهید کرد.
روش دیگر بررسی آن، اجرای دستور msinfo32 در قسمت run ویندوز و سپس enter است. در قسمت system summary، اطلاعات Second Level Address Translation قابل مشاهده هستند (اگر No باشد، امکان اجرای containerهای لینوکسی را بر روی ویندوز نخواهید داشت):
- داشتن حداقل 4 گیگابایت RAM.
- فعال بودن Hyper-V نیز برای اجرای Linux Containers بر روی ویندوز، ضروری است (نصاب Docker، اینکار را به صورت خودکار انجام میدهد).
دریافت نصاب Docker for Windows
برای دریافت نصاب داکر مخصوص ویندوز، به آدرس زیر مراجعه کنید:
https://store.docker.com/editions/community/docker-ce-desktop-windows
که بلافاصله با تصویر کریه زیر مواجه خواهید شد:
برای رفع این مشکل، میتوان از روش مطرح شدهی در مطلب «یک روش ساده برای دور زدن تحریمها!» استفاده کرد؛ یعنی تنظیم DNS به 178.22.122.100 به صورت زیر:
پس از این تغییر، چون IP قابل مشاهدهی سیستم شما توسط سایت داکر تغییر میکند، اینبار صفحهی دریافت Docker Community Edition for Windows به صورت زیر ظاهر میشود:
همانطور که مشاهده میکنید، عنوان کردهاست که لطفا لاگین کنید تا بتوانید این برنامه را دریافت کنید. به همین جهت بر روی لینک آن کلیک کرده، یک اکانت جدید را در سایت docker ایجاد کنید (با یک ایمیل واقعی که تائیدیه آنرا دریافت خواهید کرد). پس از آن، با این اکانت جدید به سایت داکر وارد شوید تا لینک دریافت فایل exe نصاب آنرا دریافت کنید.
در این حالت مرورگر و یا حتی دانلودمنیجر شما بدون مشکل میتوانند این فایل را دریافت کنند و همان تنظیم DNS فوق، مشکل عدم دسترسی را برطرف میکند.
نصب Docker for Windows
پس از اجرای نصاب آن و پایان عملیات نصب (که تنها کافی است در صفحهی ابتدایی آن تیک مربوط به Windows Containers را نیز قرار دهید)، نیاز دارد تا شما را یکبار از سیستم Logout و login کند. پس از ورود به سیستم، تنظیمات ابتدایی آن به صورت خودکار صورت گرفته و در صورت فعال نبودن Hyper-V، پیام زیر را مشاهده خواهید کرد:
بر روی OK کلیک کنید تا اینکار با موفقیت به پایان برسد. البته پس از آن، منتظر حداقل یکبار ریاستارت شدن خودکار سیستم، بدون اطلاع قبلی نیز باشید.
یک نکته: کاری که در قسمت فعالسازی Hyper-V به صورت خودکار انجام میشود، شامل اجرای سه دستور زیر، در کنسول پاور شل، با دسترسی مدیریتی و سپس ری استارت سیستم است:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -Verbose Enable-WindowsOptionalFeature -Online -FeatureName Containers -All -Verbose bcdedit /set hypervisorlaunchtype Auto
C:\Users\Vahid>docker info Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 Server Version: 18.06.1-ce OSType: windows
بررسی تنظیمات سوئیچ کردن بین Linux Containers و Windows Containers
پس از اتمام ریاستارتها، برای آزمایش فعال بودن Hyper-V، در قسمت Run ویندوز، عبارت Virtmgmt.msc را نوشته و enter کنید. اگر تصویر زیر را مشاهده نمیکنید:
یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینهی switch to Linux containers را انتخاب کنید تا پس از مدتی، آیکن MobyLinuxVM در قسمت virtual machines (تصویر فوق) ظاهر شود.
اگر پس از انتخاب این گزینه، پیام زیر را دریافت کردید:
و یا اگر بر روی این ماشین مجازی کلیک راست کردید و گزینهی Start آنرا انتخاب کردید و پیام زیر ظاهر شد:
قسمت «پیشنیازهای نصب Docker بر روی ویندوز» را با دقت بررسی کنید (خصوصا قسمت BIOS و SLAT). نبود یکی از موارد ذکر شده، سبب بروز این مشکل میشود.
برای مثال اجرای دستور coreinfo -v بر روی سیستم من چنین خروجی را به همراه دارد:
E:\>coreinfo -v AuthenticAMD Microcode signature: 00000000 HYPERVISOR - Hypervisor is present SVM * Supports AMD hardware-assisted virtualization NP - Supports AMD nested page tables (SLAT)
همانطور که مشاهده میکنید، قابلیت SLAT در CPU این سیستم وجود ندارد. به همین جهت نمیتوان به Linux containers سوئیچ کرد. هرچند windows containers آن کار میکند.
روش دیگر مشاهدهی این خطا، مراجعهی به event viewer ویندوز است. در قسمت خطاهای سیستم، ممکن است خطای زیر را مشاهده کنید:
Hypervisor launch failed; Second Level Address Translation is required to launch the hypervisor.
آزمایش Docker نصب شده
پس از نصب docker، خط فرمان ویندوز را گشوده و دستور زیر را صادر کنید:
docker run hello-world
یک نکته: این image، یک image لینوکسی است. به همین جهت پیش از اجرای این دستور، همانطور که پیشتر نیز عنوان شد، یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینهی switch to Linux containers را انتخاب کنید. سپس دستور docker run hello-world را اجرا نمائید.
و یا در همین حال دستور docker run -p 80:80 nginx را صادر کنید تا وب سرور لینوکسی nginx را بتوانید تحت ویندوز اجرا کنید. پس از خاتمهی عملیات دریافت و اجرای وب سرور، با توجه به تنظیم p 80:80-، پورت 80 میزبان (اولین عدد)، به پورت 80 کانتینر نگاشت شدهاست. به همین جهت تنها با اجرای دستور http://localhost، خروجی این وب سرور را میتوانید در مرورگر سیستم خود مشاهده کنید.
همانطور که مشاهده میکنید، با استفاده از داکر، پیش از آنکه بدانیم چگونه باید یک نرم افزار را نصب کرد، میتوان از آن استفاده کرد!
روش متوقف کردن Containers در حال اجرا
اگر دستور docker ps را در خط فرمان ویندوز اجرا کنید، لیست پروسههای اجرا شدهی توسط آن قابل مشاهده هستند. در این لیست container id در حال اجرا نیز مشخص است. برای خاتمهی کار آن، تنها کافی است دستور docker stop id را اجرا کنید.
یک نکته: ضرورتی به ذکر کامل id نیست. برای مثال ذکر سه حرف اول آن نیز کفایت میکند.
روش اجرای مجدد یک Container
فرض کنید میخواهیم سرور nginx را مجددا اجرا کنیم. یک روش آن، اجرای مجدد دستور docker run -p 80:80 nginx است که پیشتر آنرا انجام دادیم. در این حالت این image تبدیل به container شده و همانند روشهای متداول نصب نرم افزار، اکنون به عنوان یک نرم افزار نصب شده در دسترس است. برای مشاهدهی لیست آنها، دستور docker ps -a را اجرا کنید. این لیست تا این لحظه باید شامل containerهای nginx و hello-world باشد. متوقف کردن یک container، سبب تخریب یا حذف آن نمیشود. در این حالت در لیستی که توسط دستور docker ps -a نمایش داده شدهاست، باز هم container idها قابل مشاهده هستند. فقط کافی است برای اجرای یکی از آنها، دستور docker start id را اجرا کرد. به این صورت دیگر نیازی به ذکر دستور کامل docker run با تمام پارامترهای آن نیست. این id نیز همانطور که ذکر شد، میتواند سه حرف ابتدایی این id باشد تا حدی که نسبت به سایر idهای موجود، منحصربفرد شناخته شود. یا بجای container id میتوان container name نمایش داده شدهی در این لیست را استفاده کرد.
پس از اجرای nginx توسط دستور docker start id، دو روش برای بررسی در حال اجرا بودن آن وجود دارد:
الف) مرورگر را باز کنیم و آدرس http://localhost را بررسی کنیم.
ب) دستور docker ps را در خط فرمان اجرا کنیم، تا مشخص شود که آیا پروسهی nginx در حال اجرا است یا خیر؟
بنابراین دستور docker ps -a لیست تمام containers در حال اجرا و همچنین متوقف شده را نمایش میدهد. اما دستور docker ps تنها لیست containers در حال اجرا را نمایش خواهد داد.
روش حذف Containers از Docker
همانطور که در قسمت قبل نیز بحث شد، معادل نصب نرم افزار در اینجا، ایجاد یک container از یک image دریافتی از docker hub است. روش عکس آن، یعنی تخریب یک container، دقیقا معادل عزل نرم افزار از سیستم، در حالتهای متداول است. برای اینکار مجددا دستور docker ps -a را اجرا میکنیم تا لیست تمام containerهای در حال اجرا و همچنین متوقف شده نمایش داده شوند. لیستی که در اینجا نمایش داده میشود، شبیه به لیستی است که در قسمت add/remove programs ویندوز مشاهده میکنید. این لیست معادل لیست نرم افزارهای نصب شدهی بر روی سیستم است و یا برای مشاهدهی لیست imageهای دریافتی از docker hub میتوان دستور docker images را صادر کرد.
قبل از حذف یک container نیاز است آنرا متوقف کنیم. برای این منظور از دستور docker stop id استفاده میشود. سپس اجرای دستور docker rm id، سبب حذف کامل این container خواهد شد. برای آزمایش آن، مجددا دستور docker ps -a را اجرا کنید.
دستور docker rm چندین id را نیز میپذیرد. میتوان این idها و یا حتی سه حرف ابتدایی آنها را با فاصله در اینجا ذکر کرد. علاوه بر id، ذکر نام containers نیز مجاز است.
روش حذف Imageهای دریافتی از Docker Hub
دستور docker rm، فقط containers را از سیستم حذف میکند (نرم افزارهای نصب شده). اما خود imageهای اصلی دریافت شدهی از docker hub را حذف نمیکند (معادل همان فایلهای zip دریافت نرم افزار یا برنامههای نصاب، در حالت متداول و سنتی نصب نرم افزار). برای آزمایش آن دستور docker images را اجرا کنید. هنوز هم در لیست آن، تمام موارد دریافتی موجود هستند.
برای حذف یک image میتوان از دستور docker rmi id استفاده کرد (rmi بجای rm). این id نیز در لیست docker images ظاهر میشود و ذکر قسمتی از آن، تا حدی که نسبت به سایر idهای لیست شده منحصربفرد باشد، کافی است. در اینجا بجای id، از نام image نیز میتوان استفاده کرد. همچنین ذکر چندین id و یا نام نیز پس از دستور docker rmi، میسر است.
روش جستجوی imageها در Docker Hub توسط Docker CLI
فرض کنید میخواهیم image مربوط به راهنمای Docker را از Docker Hub دریافت کنیم. یک روش آن مراجعهی مستقیم به سایت آن است و استفاده از امکانات جستجوی فراهم شدهی در آن سایت. روش دیگر، استفاده از Docker CLI است. اگر دستور docker search docs را در خط فرمان اجرا کنیم، لیست تمام مخازن کدی که در آنها واژهی docs قرار دارد، نمایش داده میشود. البته پیش از نصب image آن بهتر است به برگهی tags مخزن کد آن نیز مراجعه کنید تا بتوانید حجم آنرا نیز مشاهده نمائید که حدود یک گیگابایت است. مخازن docker hub، حاوی imageهای نصاب containerهای متناظر هستند. برای دریافت و اجرای آن میتوان دستور docker run -p 4000:4000 docs/docker.github.io را اجرا کرد.
پس از دریافت یک گیگابایت مستندات، container آن بر روی پورت 4000 در سیستم ما (http://localhost:4000)، به صورت یک وب سایت استاتیک، قابل دسترسی خواهد بود. به این صورت میتوان به مستندات کامل داکر به صورت آفلاین دسترسی داشت.
مفهوم Interactive Terminal در Docker
زمانیکه دستور اجرای مستندات آفلاین را صادر میکنید، در انتهای آن عنوان میکند که وب سایت محلی آن بر روی پورت 4000 قابل دسترسی است. سپس در ذیل آن ذکر شدهاست که اگر ctrl+c را فشار دهید، اجرای آن به پایان میرسد. اما عملا اینطور نیست و اگر دستور docker ps را صادر کنید، هنوز container در حال اجرای آن را میتوان مشاهده کرد.
اما اگر اینبار دستور اجرای docker run را به همراه یک interactive terminal با سوئیچ it و نام docs صادر کنیم:
docker run -p 4000:4000 -it --name docs docs/docker.github.io
سوئیچ it یا interactive terminal سبب میشود تا یک container در foreground، بجای background اجرا شود. به این ترتیب دستور ctrl+c، سبب خاتمهی واقعی پروسهی درحال اجرای در container میشود.
روش دیگر خاتمهی این container، استفاده از نام ذکر شدهاست؛ یعنی اجرای دستور docker stop docs.
یک نکته: اگر میخواهید از terminal باز شده قطع شوید (مجددا به command prompt باز گردید) اما سبب خاتمهی container آن نشوید، از ترکیب ctrl+p+q استفاده کنید.
اجرای containerهای ویندوزی
در مورد نحوهی سوئیچ بین نوعهای مختلف containerهای ویندوزی و لینوکسی پیشتر توضیح دادیم. برای این منظور میتوان بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینهی switch to Windows/Linux containers را انتخاب کرد. باید دقت داشت که پشتیبانی از containerهای ویندوزی، از ویندوز 10، نگارش 1607، یا همان Anniversary Update آن به بعد، به ویژگیهای ویندوز اضافه شدهاند که به صورت خودکار توسط docker فعالسازی میشوند:
اجرای IIS به عنوان یک Windows Container
تا اینجا imageهای دریافتی، لینوکسی بودند. اگر گزینهی Windows Containers را به روشی که گفته شد، فعال کنید، اینبار با اجرای دستورات docker ps و یا docker images، هیچ خروجی را دریافت نخواهید کرد. از این جهت که کانتینرهای ویندوزی و لینوکسی، به صورت کاملا ایزولهای از هم اجرا و مدیریت میشوند. علت آنرا هم در MobyLinuxVM که پیشتر با اجرای دستور Virtmgmt.msc بررسی کردیم، میتوان یافت. Containerهای لینوکسی، در داخل MobyLinuxVM اجرا میشوند.
در اینجا به عنوان مثال میتوان image رسمی مربوط به IIS را از docker hub دریافت و به صورت یک کانتینر ویندوزی اجرا کرد. البته پیش از اجرای دستورات آن بهتر است به برگهی tags آن مراجعه کرده و حجمهای نگارشهای مختلف آنرا بررسی کرد. اجرای دستور docker pull microsoft/iis به معنای دریافت tag ای به نام latest است (به حجم 6 گیگابایت!)؛ یعنی با دستور docker pull microsoft/iis:latest یکی است. بنابراین در اینجا بر اساس tagهای مختلف، میتوان دستور pull متفاوتی را صادر کرد. برای مثال اگر دستور docker pull microsoft/iis:nanoserver را صادر کردید، نگارش مخصوص nano server آنرا که فقط 449 مگابایت است، دریافت میکند. بنابراین از این پس به tagهای هر مخزن docker hub خوب دقت کنید و نگارش مختص به سیستم عامل خود را دریافت نمائید. عدم ذکر tag ای، همواره tag ویژهای را به نام latest، دریافت میکند.
با اجرای دستور زیر
docker run -p 81:80 -d --name iis microsoft/iis:nanoserver
یک نکته: مشکلی با اجرای IIS مخصوص نانوسرور بر روی ویندوز 10 به این صورت و توسط داکر نیست. بنابراین پس از اجرای دستور فوق، کار دریافت image و ساخت container و سپس اجرای آن به صورت خودکار انجام شده و بلافاصله به command prompt بازگشت داده میشویم (به علت استفادهی از پارامتر d). اکنون اگر دستور docker ps را صادر کنیم، مشاهده میکنیم که کانتینر IIS مخصوص نانوسرور، هم اکنون بر روی ویندوز 10 در حال اجرا است و در آدرس http://localhost:81 قابل دسترسی است.
جهت تکمیل این بحث، بهتر است image مخصوص nanoserver را نیز از docker hub دریافت و اجرا کنیم:
docker run microsoft/windowsservercore
تنظیمات کارت شبکهی Containers
هنگامیکه پروسهای درون یک container اجرا میشود، ایزوله سازیهای بسیاری نیز در مورد آن اعمال خواهد شد؛ به همین جهت گاهی از اوقات عدهای containerها را با ماشینهای مجازی نیز مقایسه میکنند. برای مثال کانتینرها به همراه network adapter خاص خود نیز هستند؛ درست مانند اینکه یک کامپیوتر مجزای از سیستم جاری میباشند و اگر این network adapter را ping کنیم، میتوان به این صورت نیز به آن کانتینر، دسترسی داشته باشیم.
برای یافتن آن، دستور docker inspect iis را صادر میکنیم. خروجی آن به همراه یک قسمت network نیز هست که داخل آن یک IP Address قابل مشاهده است. این IP است که مختص و منحصربفرد این container است. در ابتدا برای آزمایش آن، میتوان آنرا ping کرد؛ مانند ping 172.27.49.47. همچنین به تمام برنامههای داخل این container توسط این IP نیز میتوان دسترسی یافت. برای مثال فراخوانی http://172.27.49.47:81 در مرورگر، سبب نمایش صفحهی اول IIS میشود. البته اگر اینکار را انجام دهیم، کار نمیکند. علت اینجا است، نگاشت پورتی را که تعریف کردهایم (پورت 81)، به پورتی در کامپیوتر میزبان است و نه این IP ویژه. برنامهی اصلی IIS در داخل container، به پورت 80 بر روی این آدرس IP گوش فرا میدهد. اکنون اگر آدرس http://172.27.49.47:80 را در کامپیوتر میزبان فراخوانی کنیم، کار میکند.
بنابراین هرچند containerها به معنای نرم افزارهای از پیش نصب شدهی در حال اجرا هستند، اما ... به همراه ایزوله سازیهای قابل توجهی بر روی کامپیوتر میزبان اجرا میشوند؛ درست مانند یک کامپیوتر مجزای از آن.
فعالسازی توزیع فایلهای PDB به همراه بستههای NuGet
وجود فایلهای PDB، برای اجرای برنامهها ضرورتی ندارند؛ اما اگر ارائه شوند، به کمک آنها میتوان گزارشهای استثناءهای بسیار کاملتری را به همراه نام فایل و شماره سطرهای مرتبط موجود در Exception.StackTrace، مشاهده کرد.
پیشتر توسعه دهندگان بستههای NuGet، فایلهای PDB را خودشان با تعریف یک سری include در فایل مشخصات بسته، به فایل نهایی تولیدی اضافه میکردند. اما این روزها با ارائهی «NuGet.org Symbol Server»، دیگر افزودن فایلهای PDB به بستههای nupkg توصیه نمیشود. چون حجم نهایی و زمان بازیابی بستهها را بیش از اندازه افزایش میدهند؛ خصوصا اگر مصرف کنندهای قصد دیباگ آنها را نداشته باشد.
راه حل جدید توصیه شده، ارائهی فایلهای ویژهی snupkg. در کنار بستههای nupkg. معمولی است که حاوی فایلهای PDB متناظر با بستهی اصلی NuGet هستند.
برای فعالسازی آنها در پروژههای NET Core. بستههای نیوگت خود تنها کافی است دو تنظیم زیر را به فایل csproj اضافه کنید:
<PropertyGroup> <IncludeSymbols>true</IncludeSymbols> <SymbolPackageFormat>snupkg</SymbolPackageFormat> </PropertyGroup>
در سمت مصرف کننده، IDE شما باید برای کار با این Symbol Server تنظیم شده باشد.
فعالسازی تولید Source Link
وجود PDBها جهت دیباگ بستههای ارائه شده بسیار مفیدند؛ اما اگر بر روی کدهای نهایی بهینه سازی صورت گرفته باشد، ممکن است اطلاعات دیباگ آنها با کد اصلی تطابق پیدا نکنند. برای بهبود این وضعیت و ارتقاء آن به یک سطح بالاتر، مفهوم source link ارائه شدهاست که مستقل است از نوع زبان و همچنین سورس کنترل.
کار سورسلینک، افزودن متادیتای سورس کنترل انتخابی، به بستهی نهایی تولید شدهاست. به این صورت میتوان سورس کامل و متناظر با قطعه کد بسته و کتابخانهی ثالث در حال دیباگ را در IDE خود داشت و با آن به نحو متداولی کار کرد. یعنی سورس لینک، قابلیت «Step Into" the source code"» را مهیا میکند. متادیتای اضافه شده، دقیقا مشخص میکند که بستهی تولیدی نهایی، متناظر با کدام commit سورس کنترل است. به این ترتیب دقیقا میتوان به کدهای همان commit ای که بسته بر اساس آن کامپایل شدهاست، در IDE خود دسترسی یافت.
این قابلیت از Visual Studio 15.3 به بعد در اختیار کاربران آن است (گزینهی Enable Source Server Support، در قسمت Debug/General آن باید فعال شود). همچنین Rider و VSCode نیز از آن پشتیبانی میکنند. برای Rider باید در قسمت Tools/External symbols، گزینههای Use sources from symbol files when available و Allow downloading symbols from remote locations را فعال کنید.
همچنین برای فعالسازی آن در فایل csproj بستهی نیوگت خود میتوانید تنظیمات زیر را اضافه کنید:
<PropertyGroup> <PublishRepositoryUrl>true</PublishRepositoryUrl> <EmbedUntrackedSources>true</EmbedUntrackedSources> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> </ItemGroup>
روش فعالسازی Source Link در پروژهی VSCode
اگر از VSCode استفاده میکنید، نیاز است تنظیمات زیر را به قسمت configurations فایل launch.json خود اضافه کنید تا قابلیت «Step Into" the source code"» بستههای نیوگتی که از SourceLink پشتیبانی میکنند، با فشردن دکمهی F11 در حین دیباگ، فعال شود:
"justMyCode": false, "symbolOptions": { "searchMicrosoftSymbolServer": true }, "suppressJITOptimizations": true, "env": { "COMPlus_ZapDisable": "1" }
<?xml version="1.0"?> <Configuration xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <AllowAnonymousPush>false</AllowAnonymousPush> <Repositories>D:\GitRepo</Repositories> <AllowUserRepositoryCreation>true</AllowUserRepositoryCreation> <AllowAnonymousRegistration>false</AllowAnonymousRegistration> <DefaultLanguage>en-US</DefaultLanguage> <IsCommitAuthorAvatarVisible>true</IsCommitAuthorAvatarVisible> </Configuration>
قابلیت جالبی که در اینجا به نظر من خیلی مهم بود، استخراج تاریخچهی کامل ساب ورژن توسط گیت و انتقال همه آنها به مخزن گیت است که تنها با یک خط فرمان انجام پذیر است. برای اینکار مخرنی را در گیت ساخته و آدرس .git آن را برای اجرای فرمان نگه میداریم:
البته نصب گیت برای ویندوز برای صدور فرمان انتقال به گیت الزامی است که میتوانید از این آدرس آنرا دانلود و نصب کنید.
پس از آن در 2 مرحله مخرن ساب ورژن را به گیت انتقال میدهیم:
1- استخراج آن در یک مخزن لوکال
2- افزودن به سرور گیت (که راه اندازی شده)
برای استخراج مخزنی از ساب ورژن به یک مخزن لوکال گیت، یک فولدر خالی را ایجاد میکنیم. سپس با خط فرمان به آن وارد میشویم و بعد فرمان زیر را اجرا میکنیم:
در ادامه نام کاربری و کلمهی عبور را وارد میکنیم. البته به صورت پیش فرض، نام کاربری جاری ویندوز را در نظر میگیرد و بعد نام کاربری و کلمهی عبور سرویس ساب ورژن را میپرسد و حالا گیت کارش را شروع میکند:
پس از اتمام کار با توجه به مقالهی «مراحل ارسال یک پروژهی Visual Studio به GitHub» برای کار با گیت در ویژوال استودیو، میتوان به کار با گیت بصورت ریموت ادامه دهید.
و اما نکتهی آخر: من برای استفاده از این سرور مجبور بودم که نام localhost را با نام mehdi-pc جابجا کنم تا بتوانم از طریق یک کامپیوتر دیگر با سورس کنترل کار کنم و طی جستجوهایی که در اینترنت کردم، این کار بصورت کامند و فرمانهای شبه لینوکسی انجام پذیر بود. ولی راهی را همچون این مقاله «مشکل در جابجایی پروژههای svn» پیدا کردم که بنظرم آنرا مرتبط با موضوع میدانم و گفتن آن را خالی از لطف نمیبینم.
فایل config در واقع فایل کانفیگ داخل مخزن لوکال است؛ یعنی داخل فولدر .git و بصورت متنی ذخیره شده است:
طبق انتظار قسمتی از فایل که در زیر آمده، مربوط به مشخصات اتصال به سرور ریموت میباشد:
[remote "origin"] url = http://mehdi-pc:8551/NewsService.git fetch = +refs/heads/*:refs/remotes/origin/*
البته باید بسیار با دقت این تغییر را ایجاد کنید و مطمئن باشید که آدرس را بطور صحیح و به یک مخزن درست گیت تغییر میدهید.
در فضایی که همواره هیچ تضمینی وجود ندارد که درخواست ارسال شدهی به یک API، همواره مسیر خود را همانطور که انتظار میرود طی کرده و پاسخ مورد نظر را در اختیار ما قرار میدهد، بیشک تلاش مجدد برای پردازش درخواست مورد نظر، به دلیل خطاهای گذرا، یکی از راهکارهای مورد استفاده خواهد بود. تصور کنید قصد طراحی یک مجموعه API عمومی را دارید، بهنحوی که مصرف کنندگان بدون نگرانی از ایجاد خرابی یا تغییرات ناخواسته، امکان تلاش مجدد در سناریوهای مختلف مشکل در ارتباط با سرور را داشته باشند. حتما توجه کنید که برخی از متدهای HTTP مانند GET، به اصطلاح Idempotent هستند و در طراحی آنها همواره باید این موضوع مدنظر قرار بگیرد و خروجی مشابهی برای درخواستهای تکراری همانند، مهیا کنید.
در تصویر بالا، حالتی که درخواست، توسط کلاینت ارسال شده و در آن لحظه ارتباط قطع شدهاست یا با یک خطای گذرا در سرور مواجه شدهاست و همچنین سناریویی که درخواست توسط سرور دریافت و پردازش شدهاست ولی کلاینت پاسخی را دریافت نکردهاست، قابل مشاهدهاست.
نکته: Idempotence یکی از ویژگی های پایهای عملیاتی در ریاضیات و علوم کامپیوتر است و فارغ از اینکه چندین بار اجرا شوند، نتیجه یکسانی را برای آرگومانهای همسان، خروجی خواهند داد. این خصوصیت در کانتکستهای مختلفی از جمله سیستمهای پایگاه داده و وب سرویسها قابل توجه میباشد.
Idempotent and Safe HTTP Methods
طبق HTTP RFC، متدهایی که پاسخ یکسانی را برای درخواستهای همسان مهیا میکنند، به اصطلاح Idempotent هستند. همچنین متدهایی که باعث نشوند تغییری در وضعیت سیستم در سمت سرور ایجاد شود، به اصطلاح Safe در نظر گرفته خواهند شد. برای هر دو خصوصیت عنوان شده، سناریوهای استثناء و قابل بحثی وجود دارند؛ بهعنوان مثال در مورد خصوصیت Safe بودن، درخواست GET ای را تصور کنید که یکسری لاگ آماری هم ثبت میکند یا عملیات بازنشانی کش را نیز انجام میدهد که در خیلی از موارد به عنوان یک قابلیت شناسایی خواهد شد. در این سناریوها و طبق RFC، باتوجه به اینکه هدف مصرف کننده، ایجاد Side-effect نبودهاست، هیچ مسئولیتی در قبال این تغییرات نخواهد داشت. لیست زیر شامل متدهای مختلف HTTP به همراه دو خصوصیت ذکر شده می باشد:
HTTP Method | Safe | Idempotent |
GET | Yes | Yes |
HEAD | Yes | Yes |
OPTIONS | Yes | Yes |
TRACE | Yes | Yes |
PUT | No | Yes |
DELETE | No | Yes |
POST | No | No |
PATCH | No | No |
Request Identifier as a Solution
راهکاری که عموما مورد استفاده قرار میگیرد، استفاده از یک شناسهی یکتا برای درخواست ارسالی و ارسال آن به سرور از طریق هدر HTTP می باشد. تصویر زیر از کتاب API Design Patterns، روش استفاده و مراحل جلوگیری از پردازش درخواست تکراری با شناسهای همسان را نشان میدهد:
در اینجا ابتدا مصرف کننده درخواستی با شناسه «۱» را برای پردازش به سرور ارسال میکند. سپس سرور که لیستی از شناسههای پردازش شدهی قبلی را نگهداری کردهاست، تشخیص میدهد که این درخواست قبلا دریافت شدهاست یا خیر. پس از آن، عملیات درخواستی انجام شده و شناسهی درخواست، به همراه پاسخ ارسالی به کلاینت، در فضایی ذخیره سازی میشود. در ادامه اگر همان درخواست مجددا به سمت سرور ارسال شود، بدون پردازش مجدد، پاسخ پردازش شدهی قبلی، به کلاینت تحویل داده می شود.
Implementation in .NET
ممکن است پیادهسازیهای مختلفی را از این الگوی طراحی در اینترنت مشاهده کنید که به پیاده سازی یک Middleware بسنده کردهاند و صرفا بررسی این مورد که درخواست جاری قبلا دریافت شدهاست یا خیر را جواب می دهند که ناقص است. برای اینکه اطمینان حاصل کنیم درخواست مورد نظر دریافت و پردازش شدهاست، باید در منطق عملیات مورد نظر دست برده و تغییراتی را اعمال کنیم. برای این منظور فرض کنید در بستری هستیم که می توانیم از مزایای خصوصیات ACID دیتابیس رابطهای مانند SQLite استفاده کنیم. ایده به این شکل است که شناسه درخواست دریافتی را در تراکنش مشترک با عملیات اصلی ذخیره کنیم و در صورت بروز هر گونه خطا در اصل عملیات، کل تغییرات برگشت خورده و کلاینت امکان تلاش مجدد با شناسهی مورد نظر را داشته باشد. برای این منظور مدل زیر را در نظر بگیرید:
public class IdempotentId(string id, DateTime time) { public string Id { get; private init; } = id; public DateTime Time { get; private init; } = time; }
هدف از این موجودیت ثبت و نگهداری شناسههای درخواستهای دریافتی میباشد. در ادامه واسط IIdempotencyStorage را برای مدیریت نحوه ذخیره سازی و پاکسازی شناسههای دریافتی خواهیم داشت:
public interface IIdempotencyStorage { Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken); Task CleanupOutdated(CancellationToken cancellationToken); bool IsKnownException(Exception ex); }
در اینجا متد TryPersist سعی میکند با شناسه دریافتی یک رکورد را ثبت کند و اگر تکراری باشد، خروجی false خواهد داشت. متد CleanupOutdated برای پاکسازی شناسههایی که زمان مشخصی (مثلا ۱۲ ساعت) از دریافت آنها گذشته است، استفاده خواهد شد که توسط یک وظیفهی زمانبندی شده می تواند اجرا شود؛ به این صورت، امکان استفادهی مجدد از آن شناسهها برای کلاینتها مهیا خواهد شد. پیاده سازی واسط تعریف شده، به شکل زیر خواهد بود:
/// <summary> /// To prevent from race-condition, this default implementation relies on primary key constraints. /// </summary> file sealed class IdempotencyStorage( AppDbContext dbContext, TimeProvider dateTime, ILogger<IdempotencyStorage> logger) : IIdempotencyStorage { private const string ConstraintName = "PK_IdempotentId"; public Task CleanupOutdated(CancellationToken cancellationToken) { throw new NotImplementedException(); //TODO: cleanup the outdated ids based on configurable duration } public bool IsKnownException(Exception ex) { return ex is UniqueConstraintException e && e.ConstraintName.Contains(ConstraintName); } // To tackle race-condition issue, the implementation relies on storage capabilities, such as primary constraint for given IdempotentId. public async Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken) { try { dbContext.Add(new IdempotentId(idempotentId, dateTime.GetUtcNow().UtcDateTime)); await dbContext.SaveChangesAsync(cancellationToken); return true; } catch (UniqueConstraintException e) when (e.ConstraintName.Contains(ConstraintName)) { logger.LogInformation(e, "The given idempotentId [{IdempotentId}] already exists in the storage.", idempotentId); return false; } } }
همانطور که مشخص است در اینجا سعی شدهاست تا با شناسهی دریافتی، یک رکورد جدید ثبت شود که در صورت بروز خطای UniqueConstraint، خروجی با مقدار false را خروجی خواهد داد که می توان از آن نتیجه گرفت که این درخواست قبلا دریافت و پردازش شدهاست (در ادامه نحوهی استفاده از آن را خواهیم دید).
در این پیاده سازی از کتابخانه MediatR استفاده می کنیم؛ در همین راستا برای مدیریت تراکنش ها به صورت زیر می توان TransactionBehavior را پیاده سازی کرد:
internal sealed class TransactionBehavior<TRequest, TResponse>( AppDbContext dbContext, ILogger<TransactionBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IBaseCommand where TResponse : IErrorOr { public async Task<TResponse> Handle( TRequest command, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { string commandName = typeof(TRequest).Name; await using var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); TResponse? result; try { logger.LogInformation("Begin transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command); result = await next(); if (result.IsError) { await transaction.RollbackAsync(cancellationToken); logger.LogInformation("Rollback transaction {TransactionId} for handling {CommandName} ({@Command}) due to failure result.", transaction.TransactionId, commandName, command); return result; } await transaction.CommitAsync(cancellationToken); logger.LogInformation("Commit transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command); } catch (Exception ex) { await transaction.RollbackAsync(cancellationToken); logger.LogError(ex, "An exception occured within transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command); throw; } return result; } }
در اینجا مستقیما AppDbContext تزریق شده و با استفاده از خصوصیت Database آن، کار مدیریت تراکنش انجام شدهاست. همچنین باتوجه به اینکه برای مدیریت خطاها از کتابخانهی ErrorOr استفاده می کنیم و خروجی همهی Command های سیستم، حتما یک وهله از کلاس ErrorOr است که واسط IErrorOr را پیاده سازی کردهاست، یک محدودیت روی تایپ جنریک اعمال کردیم که این رفتار، فقط برروی IBaseCommand ها اجرا شود. تعریف واسط IBaseCommand به شکل زیر میباشد:
/// <summary> /// This is marker interface which is used as a constraint of behaviors. /// </summary> public interface IBaseCommand { } public interface ICommand : IBaseCommand, IRequest<ErrorOr<Unit>> { } public interface ICommand<T> : IBaseCommand, IRequest<ErrorOr<T>> { } public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, ErrorOr<Unit>> where TCommand : ICommand { Task<ErrorOr<Unit>> IRequestHandler<TCommand, ErrorOr<Unit>>.Handle(TCommand request, CancellationToken cancellationToken) { return Handle(request, cancellationToken); } new Task<ErrorOr<Unit>> Handle(TCommand command, CancellationToken cancellationToken); } public interface ICommandHandler<in TCommand, T> : IRequestHandler<TCommand, ErrorOr<T>> where TCommand : ICommand<T> { Task<ErrorOr<T>> IRequestHandler<TCommand, ErrorOr<T>>.Handle(TCommand request, CancellationToken cancellationToken) { return Handle(request, cancellationToken); } new Task<ErrorOr<T>> Handle(TCommand command, CancellationToken cancellationToken); }
در ادامه برای پیادهسازی IdempotencyBehavior و محدود کردن آن، واسط IIdempotentCommand را به شکل زیر خواهیم داشت:
/// <summary> /// This is marker interface which is used as a constraint of behaviors. /// </summary> public interface IIdempotentCommand { string IdempotentId { get; } } public abstract class IdempotentCommand : ICommand, IIdempotentCommand { public string IdempotentId { get; init; } = string.Empty; } public abstract class IdempotentCommand<T> : ICommand<T>, IIdempotentCommand { public string IdempotentId { get; init; } = string.Empty; }
در اینجا یک پراپرتی، برای نگهداری شناسهی درخواست دریافتی با نام IdempotentId در نظر گرفته شدهاست. این پراپرتی باید از طریق مقداری که از هدر درخواست HTTP دریافت میکنیم مقداردهی شود. به عنوان مثال برای ثبت کاربر جدید، به شکل زیر باید عمل کرد:
[HttpPost] public async Task<ActionResult<long>> Register( [FromBody] RegisterUserCommand command, [FromIdempotencyToken] string idempotentId, CancellationToken cancellationToken) { command.IdempotentId = idempotentId; var result = await sender.Send(command, cancellationToken); return result.ToActionResult(); }
در اینجا از همان Command به عنوان DTO ورودی استفاده شدهاست که وابسته به سطح Backward compatibility مورد نیاز، می توان از DTO مجزایی هم استفاده کرد. سپس از طریق FromIdempotencyToken سفارشی، شناسهی درخواست، دریافت شده و بر روی command مورد نظر، تنظیم شدهاست.
رفتار سفارشی IdempotencyBehavior از ۲ بخش تشکیل شدهاست؛ در قسمت اول سعی می شود، قبل از اجرای هندلر مربوط به command مورد نظر، شناسهی دریافتی را در storage تعبیه شده ثبت کند:
internal sealed class IdempotencyBehavior<TRequest, TResponse>( IIdempotencyStorage storage, ILogger<IdempotencyBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IIdempotentCommand where TResponse : IErrorOr { public async Task<TResponse> Handle( TRequest command, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { string commandName = typeof(TRequest).Name; if (string.IsNullOrWhiteSpace(command.IdempotentId)) { logger.LogWarning( "The given command [{CommandName}] ({@Command}) marked as idempotent but has empty IdempotentId", commandName, command); return await next(); } if (await storage.TryPersist(command.IdempotentId, cancellationToken) == false) { return (dynamic)Error.Conflict( $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed."); } return await next(); } }
در اینجا IIdempotencyStorage تزریق شده و در صورتی که امکان ذخیره سازی وجود نداشته باشد، خطای Confilict که بهخطای 409 ترجمه خواهد شد، برگشت داده میشود. در غیر این صورت ادامهی عملیات اصلی باید اجرا شود. پس از آن اگر به هر دلیلی در زمان پردازش عملیات اصلی، درخواست همزمانی با همان شناسه، توسط سرور دریافت شده و پردازش شود، عملیات جاری با خطای UniqueConstaint برروی PK_IdempotentId در زمان نهایی سازی تراکنش جاری، مواجه خواهد شد. برای این منظور بخش دوم این رفتار به شکل زیر خواهد بود:
internal sealed class IdempotencyExceptionBehavior<TRequest, TResponse>(IIdempotencyStorage storage) : IPipelineBehavior<TRequest, TResponse> where TRequest : IIdempotentCommand where TResponse : IErrorOr { public async Task<TResponse> Handle( TRequest command, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(command.IdempotentId)) return await next(); string commandName = typeof(TRequest).Name; try { return await next(); } catch (Exception ex) when (storage.IsKnownException(ex)) { return (dynamic)Error.Conflict( $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed."); } } }
در اینجا عملیات اصلی در بدنه try اجرا شده و در صورت بروز خطایی مرتبط با Idempotency، خروجی Confilict برگشت داده خواهد شد. باید توجه داشت که نحوه ثبت رفتارهای تعریف شده تا اینجا باید به ترتیب زیر انجام شود:
services.AddMediatR(config => { config.RegisterServicesFromAssemblyContaining(typeof(DependencyInjection)); // maintaining the order of below behaviors is crucial. config.AddOpenBehavior(typeof(LoggingBehavior<,>)); config.AddOpenBehavior(typeof(IdempotencyExceptionBehavior<,>)); config.AddOpenBehavior(typeof(TransactionBehavior<,>)); config.AddOpenBehavior(typeof(IdempotencyBehavior<,>)); });
به این ترتیب بدنه اصلی هندلرهای موجود در سیستم هیچ تغییری نخواهند داشت و به صورت ضمنی و انتخابی، امکان تعیین command هایی که نیاز است به صورت Idempotent اجرا شوند را خواهیم داشت.
References
https://www.mscharhag.com/p/rest-api-design
Visual Studio 2017 15.7 منتشر شد
- We added support to change installation locations.
- You can Save All your pending changes before you start your update.
- The update dialog provides you even more details about your update during installation.
- C# 7.3 is included in Visual Studio version 15.7.
- We improved solution load time for C# and VB projects.
- We made numerous updates to F# and its tools, with a focus on performance.
- We reduced the time to enable IntelliSense for large .NET Core projects by 25%.
- We made Quick Info improvements and new .NET refactorings like convert
for
-to-foreach
and make private fieldsreadonly
. - We added the ability to publish ASP.NET Core applications to App Service Linux without containers.
- Live Unit Testing works with embedded pdbs and supports projects that use reference assemblies.
- The Test Explorer has more responsive icons during test runs.
- C++ developers can use CodeLens for unit testing.
- We added new rules enforcing items from the C++ Core Guidelines.
- Debugging large solutions with /Debug:fastlink PDBs is more robust.
- CMake integration supports CMake 3.11 and static analysis.
- Python projects support type hints in IntelliSense, and a Run MyPy command has been added to look for typing errors in your code.
- Conda environments are supported in Python projects.
- We added a next version of our Python debugger based on the popular open source pydevd debugger.
- TypeScript 2.8 is included in Visual Studio version 15.7.
- We improved Kestrel HTTPs support during debugging.
- We added support for JavaScript debugging with Microsoft Edge.
- The Debugger supports VSTS and GitHub Authentication for Source Link.
- IntelliTrace’s step-back debugging feature is supported for debugging .NET Core projects.
- We added IntelliTrace support for taking snapshots on exceptions.
- We removed the blocking modal dialog from branch checkouts in Git when a solution or project reload is not required.
- There is an option to choose between OpenSSL and SChannel in Git.
- You can create and associate Azure Key Vaults from within the Visual Studio IDE.
- Visual Studio Tools for Xamarin can automatically install missing Android API levels required by Xamarin.Android projects.
- The Xamarin.Forms XAML editor provides IntelliSense and quick fixes for conditional XAML.
- We added support for Azure, UWP, and additional project types in Visual Studio Build Tools.
- You can create build servers without installing all of Visual Studio.
- The Windows 10 April 2018 Update SDK - Build 17134 is the default required SDK for the Universal Windows Platform development workload.
- We added support for Visual State Management for all UWP apps and more.
- We enabled automatic updates for sideloaded APPX packages.
- You have new tools for migrating to NuGet PackageReference.
- We added support for NuGet package signatures.
- We added Service Fabric Tooling for the 6.2 Service Fabric release.
- We updated Entity Framework Tools to work with the EF 6.2 runtime and to improve reverse engineering of existing databases.
- Performance improvements in the core debugger
- Support for .NET 6 , which can be used to build web, client and mobile apps by both Windows and Mac developers, as well as improved support for developing Azure apps
- An update UI meant to reduce complexity and which will add integration with Accessibility Insights. Microsoft plans to update the icons and add support for Cascadia Code , a new fixed-width font for better readability
- Support for C++ 20 tooling. language standardization and Intellisense
- Integration of text chat into the Live Share collaboration feature
- Additional support for Git and GitHub
- Improved code search
EF Code First #12
پیاده سازی الگوی Context Per Request در برنامههای مبتنی بر EF Code first
در طراحی برنامههای چند لایه مبتنی بر EF مرسوم نیست که در هر کلاس و متدی که قرار است از امکانات آن استفاده کند، یکبار DbContext و کلاس مشتق شده از آن وهله سازی شوند؛ به این ترتیب امکان انجام امور مختلف در طی یک تراکنش از بین میرود. برای حل این مشکل الگویی مطرح شده است به نام Session/Context Per Request و یا به اشتراک گذاری یک Unit of work در لایههای مختلف برنامه در طی یک درخواست، که در ادامه یک پیاده سازی آنرا با هم مرور خواهیم کرد.
البته این سشن با سشن ASP.NET یکی نیست. در NHibernate معادل DbContextایی که در اینجا ملاحظه میکنید، Session نام دارد.
اهمیت بکارگیری الگوی Unit of work و به اشتراک گذاری آن در طی یک درخواست
در الگوی واحد کار یا همان DbContext در اینجا، تمام درخواستهای رسیده به آن، در صف قرار گرفته و تمام آنها در پایان کار، به بانک اطلاعاتی اعمال میشوند. برای مثال زمانیکه شیءایی را به یک وهله از DbContext اضافه/حذف میکنیم، یا در ادامه مقدار خاصیتی را تغییر میدهیم، هیچکدام از این تغییرات تا زمانیکه متد SaveChanges فراخوانی نشود، به بانک اطلاعاتی اعمال نخواهند شد. این مساله مزایای زیر را به همراه خواهد داشت:
الف) کارآیی بهتر
در اینجا از یک کانکشن باز شده، حداکثر استفاده صورت میگیرد. چندین و چند عملیات در طی یک batch به بانک اطلاعاتی اعمال میگردند؛ بجای اینکه برای اعمال هرکدام، یکبار اتصال جداگانهای به بانک اطلاعاتی باز شود.
ب) بررسی مسایل همزمانی
استفاده از یک الگوی واحد کار، امکان بررسی خودکار تمام تغییرات انجام شده بر روی یک موجودیت را در متدها و لایههای مختلف میسر کرده و به این ترتیب مسایل مرتبط با ConcurrencyMode عنوان شده در قسمتهای قبل به نحو بهتری قابل مدیریت خواهند بود.
ج) استفاده صحیح از تراکنشها
الگوی واحد کار به صورت خودکار از تراکنشها استفاده میکند. اگر در حین فراخوانی متد SaveChanges مشکلی رخ دهد، کل عملیات Rollback خواهد شد و تغییری در بانک اطلاعاتی رخ نخواهد داد. بنابراین استفاده از یک تراکنش در حین چند عملیات ناشی از لایههای مختلف برنامه، منطقیتر است تا اینکه هر کدام، در تراکنشی جدا مشغول به کار باشند.
کلاسهای مدل مثال جاری
در مثالی که در این قسمت بررسی خواهیم کرد، از کلاسهای مدل گروه محصولات کمک گرفته شده است:
using System.Collections.Generic;
namespace EF_Sample07.DomainClasses { public class Category { public int Id { get; set; } public virtual string Name { get; set; } public virtual string Title { get; set; } public virtual ICollection<Product> Products { get; set; } } }
using System.ComponentModel.DataAnnotations;
namespace EF_Sample07.DomainClasses { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; }
[ForeignKey("CategoryId")] public virtual Category Category { get; set; } public int CategoryId { get; set; } } }
در کلاس Product، یک خاصیت اضافی به نام CategoryId اضافه شده است که توسط ویژگی ForeignKey، به عنوان کلید خارجی جدول معرفی خواهد شد. از این خاصیت در برنامههای ASP.NET برای مقدار دهی یک کلید خارجی توسط یک DropDownList پر شده با لیست گروهها، استفاده خواهیم کرد.
پیاده سازی الگوی واحد کار
همانطور که در قسمت قبل نیز ذکر شد، DbContext در EF Code first بر اساس الگوی واحد کار تهیه شده است، اما برای به اشتراک گذاشتن آن بین لایههای مختلف برنامه نیاز است یک لایه انتزاعی را برای آن تهیه کنیم، تا بتوان آنرا به صورت خودکار توسط کتابخانههای Dependency Injection یا به اختصار DI در زمان نیاز به استفاده از آن، به کلاسهای استفاده کننده تزریق کنیم. کتابخانهی DI ایی که در این قسمت مورد استفاده قرار میگیرد، کتابخانه معروف StructureMap است. برای دریافت آن میتوانید از Nuget استفاده کنید؛ یا از صفحه اصلی آن در Github : (^).
اینترفیس پایه الگوی واحد کار ما به شرح زیر است:
using System.Data.Entity; using System;
namespace EF_Sample07.DataLayer.Context { public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveChanges(); } }
برای استفاده اولیه آن، تنها تغییری که در برنامه حاصل میشود به نحو زیر است:
using System.Data.Entity; using EF_Sample07.DomainClasses;
namespace EF_Sample07.DataLayer.Context { public class Sample07Context : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; }
#region IUnitOfWork Members public new IDbSet<TEntity> Set<TEntity>() where TEntity : class { return base.Set<TEntity>(); } #endregion } }
توضیحات:
با کلاس Context در قسمتهای قبل آشنا شدهایم. در اینجا به معرفی کلاسهایی خواهیم پرداخت که در معرض دید EF Code first قرار خواهند گرفت.
DbSetها هم معرف الگوی Repository هستند. کلاس Sample07Context، معرفی الگوی واحد کار یا Unit of work برنامه است.
برای اینکه بتوانیم تعاریف کلاسهای سرویس برنامه را مستقل از تعریف کلاس Sample07Context کنیم، یک اینترفیس جدید را به نام IUnitOfWork به برنامه اضافه کردهایم.
در اینجا کلاس Sample07Context پیاده سازی کننده اینترفیس IUnitOfWork خواهد بود (اولین تغییر).
دومین تغییر هم استفاده از متد base.Set میباشد. به این ترتیب به سادگی میتوان به DbSetهای مختلف در حین کار با IUnitOfWork دسترسی پیدا کرد. به عبارتی ضرورتی ندارد به ازای تک تک DbSetها یکبار خاصیت جدیدی را به اینترفیس IUnitOfWork اضافه کرد. به کمک استفاده از امکانات Generics مهیا، اینبار
uow.Set<Product>
معادل همان db.Products سابق است؛ در حالتیکه از Sample07Context به صورت مستقیم استفاده شود.
همچنین نیازی به پیاده سازی متد SaveChanges نیست؛ زیرا پیاده سازی آن در کلاس DbContext قرار دارد.
استفاده از الگوی واحد کار در کلاسهای لایه سرویس برنامه
using EF_Sample07.DomainClasses; using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer { public interface ICategoryService { void AddNewCategory(Category category); IList<Category> GetAllCategories(); } }
using EF_Sample07.DomainClasses; using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer { public interface IProductService { void AddNewProduct(Product product); IList<Product> GetAllProducts(); } }
لایه سرویس برنامه را با دو اینترفیس جدید شروع میکنیم. هدف از این اینترفیسها، ارائه پیاده سازیهای متفاوت، به ازای ORMهای مختلف است. برای مثال در کلاسهای زیر که نام آنها با Ef شروع شده است، پیاده سازی خاص Ef Code first را تدارک خواهیم دید. این پیاده سازی، قابل انتقال به سایر ORMها نیست چون نه پیاده سازی یکسانی را از مباحث LINQ ارائه میدهند و نه متدهای الحاقی همانندی را به همراه دارند و نه اینکه مباحث نگاشت کلاسهای آنها به جداول مختلف یکی است:
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer { public class EfCategoryService : ICategoryService { IUnitOfWork _uow; IDbSet<Category> _categories; public EfCategoryService(IUnitOfWork uow) { _uow = uow; _categories = _uow.Set<Category>(); }
public void AddNewCategory(Category category) { _categories.Add(category); }
public IList<Category> GetAllCategories() { return _categories.ToList(); } } }
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer { public class EfProductService : IProductService { IUnitOfWork _uow; IDbSet<Product> _products; public EfProductService(IUnitOfWork uow) { _uow = uow; _products = _uow.Set<Product>(); }
public void AddNewProduct(Product product) { _products.Add(product); }
public IList<Product> GetAllProducts() { return _products.Include(x => x.Category).ToList(); } } }
توضیحات:
همانطور که ملاحظه میکنید در هیچکدام از کلاسهای سرویس برنامه، وهله سازی مستقیمی از الگوی واحد کار وجود ندارد. این لایه از برنامه اصلا نمیداند که کلاسی به نام Sample07Context وجود خارجی دارد یا خیر.
همچنین لایه اضافی دیگری را به نام Repository جهت مخفی سازی سازوکار EF به برنامه اضافه نکردهایم. این لایه شاید در نگاه اول برنامه را مستقل از ORM جلوه دهد اما در عمل قابل انتقال نیست و سبب تحمیل سربار اضافی بی موردی به برنامه میشود؛ ORMها ویژگیهای یکسانی را ارائه نمیدهند. حتی در حالت استفاده از LINQ، پیاده سازیهای یکسانی را به همراه ندارند.
بنابراین اگر قرار است برنامه مستقل از ORM کار کند، نیاز است لایه استفاده کننده از سرویس برنامه، با دو اینترفیس IProductService و ICategoryService کار کند و نه به صورت مستقیم با پیاده سازی آنها. به این ترتیب هر زمان که لازم شد، فقط باید پیاده سازیهای کلاسهای سرویس را تغییر داد؛ باز هم برنامه نهایی بدون نیاز به تغییری کار خواهد کرد.
تا اینجا به معماری پیچیدهای نرسیدهایم و اصطلاحا over-engineering صورت نگرفته است. یک اینترفیس بسیار ساده IUnitOfWork به برنامه اضافه شده؛ در ادامه این اینترفیس به کلاسهای سرویس برنامه تزریق شده است (تزریق وابستگی در سازنده کلاس). کلاسهای سرویس ما «میدانند» که EF وجود خارجی دارد و سعی نکردهایم توسط لایه اضافی دیگری آنرا مخفی کنیم. شیوه کار با IDbSet تعریف شده دقیقا همانند روال متداولی است که با EF Code first کار میشود و بسیار طبیعی جلوه میکند.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه کنسول ویندوزی
در ادامه برای وهله سازی اینترفیسهای سرویس و واحد کار برنامه، از کتابخانه StructureMap که یاد شد، استفاده خواهیم کرد. بنابراین، تمام برنامههای نهایی ارائه شده در این قسمت، ارجاعی را به اسمبلی StructureMap.dll نیاز خواهند داشت.
کدهای برنامه کنسول مثال جاری را در ادامه ملاحظه خواهید کرد:
using System.Collections.Generic; using System.Data.Entity; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07 { class Program { static void Main(string[] args) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().CacheBy(InstanceScope.Hybrid).Use<Sample07Context>(); x.For<ICategoryService>().Use<EfCategoryService>(); });
var uow = ObjectFactory.GetInstance<IUnitOfWork>(); var categoryService = ObjectFactory.GetInstance<ICategoryService>();
var product1 = new Product { Name = "P100", Price = 100 }; var product2 = new Product { Name = "P200", Price = 200 }; var category1 = new Category { Name = "Cat100", Title = "Title100", Products = new List<Product> { product1, product2 } }; categoryService.AddNewCategory(category1); uow.SaveChanges(); } } }
در اینجا بیشتر هدف، معرفی نحوه استفاده از StructureMap است.
ابتدا توسط متد ObjectFactory.Initialize مشخص میکنیم که اگر برنامه نیاز به اینترفیس IUnitOfWork داشت، لطفا کلاس Sample07Context را وهله سازی کرده و مورد استفاده قرار بده. اگر ICategoryService مورد استفاده قرار گرفت، وهله مورد نظر باید از کلاس EfCategoryService تامین شود.
توسط ObjectFactory.GetInstance نیز میتوان به وهلهای از این کلاسها دست یافت و نهایتا با فراخوانی uow.SaveChanges میتوان اطلاعات را ذخیره کرد.
چند نکته:
- به کمک کتابخانه StructureMap، تزریق IUnitOfWork به سازنده کلاس EfCategoryService به صورت خودکار انجام میشود. اگر به کدهای فوق دقت کنید ما فقط با اینترفیسها مشغول به کار هستیم، اما وهلهسازیها در پشت صحنه انجام میشود.
- حین معرفی IUnitOfWork از متد CacheBy با پارامتر InstanceScope.Hybrid استفاده شده است. این enum مقادیر زیر را میتواند بپذیرد:
public enum InstanceScope { PerRequest = 0, Singleton = 1, ThreadLocal = 2, HttpContext = 3, Hybrid = 4, HttpSession = 5, HybridHttpSession = 6, Unique = 7, Transient = 8, }
برای مثال اگر در برنامهای نیاز داشتید یک کلاس به صورت Singleton عمل کند، فقط کافی است نحوه کش شدن آنرا تغییر دهید.
حالت PerRequest در برنامههای وب کاربرد دارد (و حالت پیش فرض است). با انتخاب آن وهله سازی کلاس مورد نظر به ازای هر درخواست رسیده انجام خواهد شد.
در حالت ThreadLocal، به ازای هر Thread، وهلهای متفاوت در اختیار مصرف کننده قرار میگیرد.
با انتخاب حالت HttpContext، به ازای هر HttpContext ایجاد شده، کلاس معرفی شده یکبار وهله سازی میگردد.
حالت Hybrid ترکیبی است از حالتهای HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد کرد در غیراینصورت به ThreadLocal سوئیچ میکند.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه ASP.NET MVC
یک برنامه خالی ASP.NET MVC را آغاز کنید. سپس یک HomeController جدید را نیز به آن اضافه نمائید و کدهای آنرا مطابق اطلاعات زیر تغییر دهید:
using System.Web.Mvc; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer; using EF_Sample07.DataLayer.Context; using System.Collections.Generic;
namespace EF_Sample07.MvcAppSample.Controllers { public class HomeController : Controller { IProductService _productService; ICategoryService _categoryService; IUnitOfWork _uow; public HomeController(IUnitOfWork uow, IProductService productService, ICategoryService categoryService) { _productService = productService; _categoryService = categoryService; _uow = uow; }
[HttpGet] public ActionResult Index() { var list = _productService.GetAllProducts(); return View(list); }
[HttpGet] public ActionResult Create() { ViewBag.CategoriesList = new SelectList(_categoryService.GetAllCategories(), "Id", "Name"); return View(); }
[HttpPost] public ActionResult Create(Product product) { if (this.ModelState.IsValid) { _productService.AddNewProduct(product); _uow.SaveChanges(); }
return RedirectToAction("Index"); }
[HttpGet] public ActionResult CreateCategory() { return View(); }
[HttpPost] public ActionResult CreateCategory(Category category) { if (this.ModelState.IsValid) { _categoryService.AddNewCategory(category); _uow.SaveChanges(); }
return RedirectToAction("Index"); } } }
نکته مهم این کنترلر، تزریق وابستگیها در سازنده کلاس کنترلر است؛ به این ترتیب کنترلر جاری نمیداند که با کدام پیاده سازی خاصی از این اینترفیسها قرار است کار کند.
اگر برنامه را به همین نحو اجرا کنیم، موتور ASP.NET MVC ایراد خواهد گرفت که یک کنترلر باید دارای سازندهای بدون پارامتر باشد تا من بتوانم به صورت خودکار وهلهای از آنرا ایجاد کنم. برای رفع این مشکل از کتابخانه StructureMap برای تزریق خودکار وابستگیها کمک خواهیم گرفت:
using System; using System.Data.Entity; using System.Web.Mvc; using System.Web.Routing; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07.MvcAppSample
{ // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); }
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
protected void Application_Start() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); initStructureMap(); }
private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>(); });
//Set current Controller factory as StructureMapControllerFactory ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); }
protected void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } }
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return ObjectFactory.GetInstance(controllerType) as Controller; } } }
توضیحات:
کدهای فوق متعلق به کلاس Global.asax.cs هستند. در اینجا در متد Application_Start، متد initStructureMap فراخوانی شده است.
با پیاده سازی ObjectFactory.Initialize در کدهای برنامه کنسول معرفی شده آشنا شدیم. اینبار فقط حالت کش شدن کلاس Context برنامه را HttpContextScoped قرار دادهایم تا به ازای هر درخواست رسیده یک بار الگوی واحد کار وهله سازی شود.
نکته مهمی که در اینجا اضافه شدهاست، استفاده از متد ControllerBuilder.Current.SetControllerFactory میباشد. این متد نیاز به وهلهای از نوع DefaultControllerFactory دارد که نمونهای از آنرا در کلاس StructureMapControllerFactory مشاهده میکنید. به این ترتیب در زمان وهله سازی خودکار یک کنترلر، اینبار StructureMap وارد عمل شده و وابستگیهای برنامه را مطابق تعاریف ObjectFactory.Initialize ذکر شده، به سازنده کلاس کنترلر تزریق میکند.
همچنین در متد Application_EndRequest با فراخوانی ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects از نشتی اتصالات به بانک اطلاعاتی جلوگیری خواهیم کرد. چون وهله الگوی کار برنامه HttpScoped تعریف شده، در پایان یک درخواست به صورت خودکار توسط StructureMap پاکسازی میشود و به نشتی منابع نخواهیم رسید.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه ASP.NET Web forms
در یک برنامه ASP.NET Web forms نیز میتوان این مباحث را پیاده سازی کرد:
using System; using System.Data.Entity; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07.WebFormsAppSample { public class Global : System.Web.HttpApplication { private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();
x.SetAllProperties(y=> { y.OfType<IUnitOfWork>(); y.OfType<ICategoryService>(); y.OfType<IProductService>(); }); }); }
void Application_Start(object sender, EventArgs e) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); initStructureMap(); }
void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); }
در اینجا کدهای کلاس Global.asax.cs را ملاحظه میکنید. توضیحات آن با قسمت ASP.NET MVC آنچنان تفاوتی ندارد و یکی است. البته منهای تعاریف SetAllProperties که جدید است و در ادامه به علت اضافه کردن آنها خواهیم رسید.
در ASP.NET Web forms برخلاف ASP.NET MVC نیاز است کار وهله سازی اینترفیسها را به صورت دستی انجام دهیم. برای این منظور و کاهش کدهای تکراری برنامه میتوان یک کلاس پایه را به نحو زیر تعریف کرد:
using System.Web.UI; using StructureMap;
namespace EF_Sample07.WebFormsAppSample { public class BasePage : Page { public BasePage() { ObjectFactory.BuildUp(this); } } }
سپس برای استفاده از آن خواهیم داشت:
using System; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer;
namespace EF_Sample07.WebFormsAppSample { public partial class AddProduct : BasePage { public IUnitOfWork UoW { set; get; } public IProductService ProductService { set; get; } public ICategoryService CategoryService { set; get; }
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { bindToCategories(); } }
private void bindToCategories() { ddlCategories.DataTextField = "Name"; ddlCategories.DataValueField = "Id"; ddlCategories.DataSource = CategoryService.GetAllCategories(); ddlCategories.DataBind(); }
protected void btnAdd_Click(object sender, EventArgs e) { var product = new Product { Name = txtName.Text, Price = int.Parse(txtPrice.Text), CategoryId = int.Parse(ddlCategories.SelectedItem.Value) }; ProductService.AddNewProduct(product); UoW.SaveChanges(); Response.Redirect("~/Default.aspx"); } } }
اینبار وابستگیهای کلاس افزودن محصولات، به صورت خواصی عمومی تعریف شدهاند. این خواص عمومی توسط متد SetAllProperties که در فایل global.asax.cs معرفی شدند، باید یکبار تعریف شوند (مهم!).
سپس اگر دقت کرده باشید، اینبار کلاس AddProduct از BasePage ما ارث بری کرده است. در سازند کلاس BasePage، با فراخوانی متد ObjectFactory.BuildUp، تزریق وابستگیها به خواص عمومی کلاس جاری صورت میگیرد.
در ادامه نحوه استفاده از این اینترفیسها را جهت مقدار دهی یک DropDownList یا ذخیره سازی اطلاعات یک محصول مشاهده میکنید. در اینجا نیز کار با اینترفیسها انجام شده و کلاس جاری دقیقا نمیداند که با چه وهلهای مشغول به کار است. تنها در زمان اجرا است که توسط StructureMap ، به ازای هر اینترفیس معرفی شده، وهلهای مناسب بر اساس تعاریف فایل Global.asax.cs در اختیار برنامه قرار میگیرد.
کدهای کامل مثالهای این سری را از آدرس زیر هم میتوانید دریافت کنید: (^)
به روز رسانی
کدهای قسمت جاری را به روز شده جهت استفاده از EF 6 و StructureMap 3 در VS 2013، از اینجا میتوانید دریافت کنید:
EF_Sample07