سؤال شما بیشتر به مطلب «CheckBoxList در ASP.NET MVC » مرتبط است تا enum ایی که ویژگی flag دارد. ضمنا گرید تلریک مستندات خوبی دارد که بهتر است به آن مراجعه کنید (مثال Ajax CheckBoxes هست که کدهای View و کنترلر آن نیز پیوست شدند).
- در مورد صفحه بندی اطلاعات در MVC، مطلب زیاد وجود دارد.
- در برنامههای MVC عموما از افزونههای گریدهای مبتنی بر jQuery برای اینکار استفاده میشود. مانند kendo ui و یا jqGrid و یا DataTables و ... (اینها مثالهای NET Core. هم دارند: ^ و ^) و یا «پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery» و یا WebGrid
- و یا ترکیبی از «مرتبسازی، فیلتر کردن و صفحهبندی اطلاعات در ASP.NET Core» و افزونهی « cloudscribe.Web.Pagination ». یک مثال: ^ و ^
برای بسیاری از تازه کاران که پا به عرصهی برنامههای تحت وب میگذارند، اینکه چگونه، از کجا و چطور باید هاستی را انتخاب کنند، دچار سردرگمی هستند. دیدن پلنهای مختلف با قیمتهای مختلف، باعث افزایش سردرگمی آنها میشود. در این مقاله به بررسی اینکه چطور باید هاستی خریداری شود و اینکه اصلا خود برنامهی نوشته شده نیازش چقدر هست، صحبت میکنیم.
قبل از اینکه صحبت را آغاز کنیم باید این نکته اشاره کنیم که انواع هاست چیست؟
انواع هاست
هاستها بر سه نوع تقسیم میشوند:
هاست اشتراکی
در این حالت شرکت میزبان یک سرور را به چند سایت تقسیم کرده و به هر وب سایت، با توجه به پلنی که مشتری انتخاب کرده، مقداری از منابع را اختصاص میدهد که عموما این تخصیص منابع در زمان خرید توسط WHMCS به طور خودکار صورت میگیرد. در این حالت ممکن است بر روی یک سرور بیش از یکصد وب سایت در حال سرویس گرفتن باشند. مزیت این هاستها قیمت ارزان آنها میباشد . عموما وب سایتهای با بازدید کم و نیاز به منابع کمتر، از این دست هاستها استفاده میکنند. در این نوع هاستها در صورتیکه استفادهی از منابع به نهایت مقداری که برای آن مشخص شده است برسد، از قبیل ترافیک (که بیشتر مسئله مربوط به آن است) یا دیسک سخت و ... به حد مشخص شده برسند، سایت را متوقف یا suspend میکنند. پرداخت این نوع هاستها به خاطر قیمت پایین به صورت یکساله دریافت میشود. قیمست سالیانه آنها در بعضی جاها از 50 هزار تومان تا صدهزار تومان به عنوان مبلغ آغازین شروع میشود.
سرورهای مجازی VPS یا Virtual Private Server
اینها هم تقریبا مثل هاستهای اشتراکی هستند با این تفاوت که منابع بیشتر و دسترسی بیشتری به شما داده میشوند؛ به طوری که احساس میکنید به شما یک سرور واقعی را دادهاند و عموما تعداد سایت هایی که روی آن سرویس میگیرند، به مراتب کمتر از هاست اشتراکی است. اکثر وب سایتهایی که توانایی استفاده از هاستهای اشتراکی را ندارند، از این نوع هاستینگ بهره میبرند. قیمتش بالاتر از یک هاست اشتراکی است ولی به مراتب پایینتر از یک سرور اختصاصی است. پرداخت این نوع هاست عموما به دو صورت ماهیانه و یا سالانه است که احتمال زیادی دارد پرداخت سالیانه تخفیف خوبی را شامل شود. در صورت رسیدن به حد نهایت منابع همانند هاست اشتراکی با شما رفتار خواهد شد. این سرورهای مجازی در ایران عموما از مبلغ ماهیانه 20 هزار تومان به بالا آغاز میشوند.
سروهای اختصاصی یا Dedicate
اینها دیگر سروهای واقعی هستند و صاحب اول و آخرشان شمایید و دسترسی کامل به همه اجزا و منابع آن را دارید. این سرورهای عموما برای فعالیتهای بزرگ تجاری و بازدیدهای همزمان به شدت بالا استفاده میشوند. قیمت، بسته به مشخصات آن متفاوت است و ممکن است یکی ماهیانه 200 هزار تومان، یکی ماهیانه 500 هزار تومان و .. آغاز شود.
سرورهای ابری
اگر قصد انجام عملیات رایانش برای را دارید بهتر هست که دنبال چنین سرورهایی باشید که مورد بحث این مقاله نیست و صرفا جهت تکمیل معرفی انواع هاستها آمده است.
قبل از اینکه صحبت را آغاز کنیم باید این نکته اشاره کنیم که انواع هاست چیست؟
انواع هاست
هاستها بر سه نوع تقسیم میشوند:
- هاست اشتراکی
- سرورهای مجازی
- سرور اختصاصی
هاست اشتراکی
در این حالت شرکت میزبان یک سرور را به چند سایت تقسیم کرده و به هر وب سایت، با توجه به پلنی که مشتری انتخاب کرده، مقداری از منابع را اختصاص میدهد که عموما این تخصیص منابع در زمان خرید توسط WHMCS به طور خودکار صورت میگیرد. در این حالت ممکن است بر روی یک سرور بیش از یکصد وب سایت در حال سرویس گرفتن باشند. مزیت این هاستها قیمت ارزان آنها میباشد . عموما وب سایتهای با بازدید کم و نیاز به منابع کمتر، از این دست هاستها استفاده میکنند. در این نوع هاستها در صورتیکه استفادهی از منابع به نهایت مقداری که برای آن مشخص شده است برسد، از قبیل ترافیک (که بیشتر مسئله مربوط به آن است) یا دیسک سخت و ... به حد مشخص شده برسند، سایت را متوقف یا suspend میکنند. پرداخت این نوع هاستها به خاطر قیمت پایین به صورت یکساله دریافت میشود. قیمست سالیانه آنها در بعضی جاها از 50 هزار تومان تا صدهزار تومان به عنوان مبلغ آغازین شروع میشود.
سرورهای مجازی VPS یا Virtual Private Server
اینها هم تقریبا مثل هاستهای اشتراکی هستند با این تفاوت که منابع بیشتر و دسترسی بیشتری به شما داده میشوند؛ به طوری که احساس میکنید به شما یک سرور واقعی را دادهاند و عموما تعداد سایت هایی که روی آن سرویس میگیرند، به مراتب کمتر از هاست اشتراکی است. اکثر وب سایتهایی که توانایی استفاده از هاستهای اشتراکی را ندارند، از این نوع هاستینگ بهره میبرند. قیمتش بالاتر از یک هاست اشتراکی است ولی به مراتب پایینتر از یک سرور اختصاصی است. پرداخت این نوع هاست عموما به دو صورت ماهیانه و یا سالانه است که احتمال زیادی دارد پرداخت سالیانه تخفیف خوبی را شامل شود. در صورت رسیدن به حد نهایت منابع همانند هاست اشتراکی با شما رفتار خواهد شد. این سرورهای مجازی در ایران عموما از مبلغ ماهیانه 20 هزار تومان به بالا آغاز میشوند.
سروهای اختصاصی یا Dedicate
اینها دیگر سروهای واقعی هستند و صاحب اول و آخرشان شمایید و دسترسی کامل به همه اجزا و منابع آن را دارید. این سرورهای عموما برای فعالیتهای بزرگ تجاری و بازدیدهای همزمان به شدت بالا استفاده میشوند. قیمت، بسته به مشخصات آن متفاوت است و ممکن است یکی ماهیانه 200 هزار تومان، یکی ماهیانه 500 هزار تومان و .. آغاز شود.
نکته ای در مورد سرورهای اختصاصی و حتی گاها VPSها هست اینکه عموما پشتیبانی اینها توسط شما تامین میشود و یا باید یک مدیر سرور با حق ماهیانه اختیار کنید یا در صورت بروز مشکل به صورت ساعتی حق حل مشکل را بدهید. بهتر هست این مورد را از قبل توسط میزبان جویا شوید.
سرورهای ابری
اگر قصد انجام عملیات رایانش برای را دارید بهتر هست که دنبال چنین سرورهایی باشید که مورد بحث این مقاله نیست و صرفا جهت تکمیل معرفی انواع هاستها آمده است.
چک لیست
موقعی که شما نوع هاست خود را انتخاب میکنید به یک سری پلن یا پکیجهای مختلفی میرسید که مشخصات مختلفی دارند و هر شرکت مبلغ و پیکربندی مختلفی را در نظر میگیرد. نگاهی به چک لیست پایین و بررسی گام به گام آن میتواند شما را در رسیدن به انتخاب بهتر یاری کند.
فضای دیسک و پهنای باند (ترافیک)
اولین نکاتی که همیشه توسط میزبانها و خریداران مورد توجه قرار میگیرند، این دو مورد هست. در صورتیکه سایت شما اجازه آپلودی به کاربران نمیدهد، یا خودتان هم آپلود چندانی ندارید، میتوانید فضای دیسک سخت پایینتری را انتخاب کنید. مثلا اگر شما تعدادی تصویر دارید و مقدار زیادی فایل متنی و ... به نظر یک گیگ کفایت میکند و در صورتیکه بعدها دیدید این فضا رو به اتمام است، میتوانید به میزبان درخواست افزایش و ارتقاء آن را بدهید؛ اما برای بار اول یک گیگ به خوبی کفایت میکند. ولی اگر چنانچه با فایلهای حجیم سرو کله میزنید و یا تعداد فایلهایی چون تصاویر، صوت و ویدیو در آن زیاد دیده میشود، بهتر است به فکر فضایی بیشتر و حتی گاها unlimited باشید. برای سایتهایی که حجم به شدت بالایی دارند و یا مثلا نیاز به راه اندازی بخش دانلود دارند، بهتر هست از یک هاستینگ به نام هاست دانلود استفاده کنند. این نوع هاستها به شما اجازه میزبانی فایلهای وب سایتهایی چون aspx,mvc,php را نمیدهند و بیشتر پهنای باند و فضای دیسک سخت آن مدنظر است. در این حالت سایت خود را روی یک هاست معمولی به اشتراک میگذارید و فایلهای خود را روی هاست دانلود قرار میدهید و ارتباط آنها را لینک میکنید.
در مورد پهنای باند باید تعداد کاربر و همچنین نوع اطلاعاتی که جابجا میشوند را مورد بررسی قرار دهید. اگر تعداد کاربران پایین است عموما این مقدار کم است و یا اگر اطلاعات متنی فقط جابجا میشود این مقدار کمتر هم میشود.
در سناریوی اول یک انجمن VB را بررسی میکنیم: عموم انجمنها در زمینهی چند رسانهای مثل تصاویر و ...، چیزی جز تصاویر پوسته خود (که کش هم میشوند) را انتقال نمیدهند. به این دلیل که اجازهی آپلود را به کار نمیدهند و کاربرها بیشتر از سرویسهای ثالث برای تصاویر بهره میبرند و به غیر از پوستهی، سایت متون انجمن هستند که متن هم ترافیک پایینی را مصرف میکند. پس در این حالت ترافیک ماهیانهی 10 گیگ میتواند برای شروع کار تا مدتی که سایت بازدیدکنندههای خودش را پیدا کند، مناسب باشد. از نظر دیسک سخت هم به نظر من اگر شما نیاز به قرار دادن تصاویری چون اسلایدشوها و ... را دارید، به انضمام آواتار کاربران، 500 مگابایت میتواند کافی باشد. حجم آواتار کاربران در قسمت مدیریت، کنترل شده است و تنظیم دستی آن میتواند این مقدار حجم آواتارها را افزایش دهد.
در سناریوی دوم ما یک وب سرویس مثلا برای یک برنامهی اندرویدی را مثال میزنیم: عموما وب سرویسها چیزی جز یک فایل متنی را انتقال نمیدهند؛ مگر اینکه از تصاویر، صورت و ویدیو هم بهره برده باشید. در صورتیکه بیشتر همان متن را بخواهید انتقال دهید، ترافیکی جز همان متن مصرف نمیشود و حتی از یک انجمن هم مصرف پایینتری دارد و میتواند ماهیانه 10 گیگ یا حتی کمتر هم مورد استفاده قرار بگیرد. در صورت اضافه شدن تصویر و دیگر موارد چندرسانهای و کاربران در حال افزایش، این مقدار هم به همان تناسب بالا میرود.
سرعت Speed و توان سرویس دهی(Uptime)
بعد از ارزیابی موارد بالا نوبت به این دو مورد میرسد. اینکه هاست شما به قولی چقدر دوام و ایستادگی دارد، بسیار مهم است و در صورتیکه این امر محقق نگردد، میتواند موجب از دست دادن و شنیدن اعتراض کاربرها باشید. uptime به معنی این است که در یک دورهی زمانی چقدر وب سایت شما در دسترس بوده است؛ درست شنیدید! حتما پیش خودتان میگویید خوب، یک وب سایت همیشه در دسترس است. ولی واقعیت را بخواهید خیر، اینگونه نیست. اگر سرویس دهندهی خوبی را انتخاب نکنید، ممکن است در روز یا هفته یا در هرمقطع زمانی، با در دسترس نبودن وب سایت روبرو شوید؛ تقریبا مشابه زمانیکه adsl شما قطع میشود، چند لحظهای (طولانیتر از زمان عادی) مرورگر شما سعی کرده و میبیند که زورش نمیرسد و یک پیام عدم دسترسی را به شما نمایش میدهد.
به خوبی به یاد دارم که یک میزبان، هاستهای ارزان قیمتی را ارائه میکرد، ولی گاها در روز دو الی سه بار پنج دقیقهای سرور از دسترس خارج میشد و میگفتیم این هاست برای کسانی است که پول کافی ندارند و یا خیلی نمیخواهند خرج کنند و حالا پنج دقیقهای هم در روز در دسترس نباشد اهمیتی ندارد. در سایتها بیشتر این مورد را با اعدادی شبیه 99.9% مشخص میکنند و پیشنهاد میکنم هاستی را بخرید که این عدد را به شما نشان میدهد، یا حداقل دیگر 99.5% کمتر نباشد. البته تمامی هاستها همین را مینویسند، ولی واقعیت چیز دیگری است و بهتر از از دوستان و آشنایان و جستجوی در اینترنت و انجمنها به این مورد برسید.
فاکتور بعدی سرعت سرور است و کاربران به این مورد هم اهمیت ویژهای میدهند. به خوبی به یاد دارم، اولین هاستی که در کارم استفاده کردم و به پیشنهاد دوستم که در آن کار میکرد این هاست را خریداری کردم، اوایل خوب بود، ولی مشکل سرعت بعدها اضافه شد که حتی ورود به پنل مدیریتی چند دقیقهای طول میکشید. یا اینکه چند وقت پیش وب سایتی را مطالعه میکردم که فقط متن جابجا میکرد، ولی به طوری که کندتر از زمان اجرای یک سایت با گرافیک متوسط طول میکشید. (البته این نکته حائز اهمیت است خود وب سایت هم باید از این لحاظ مورد بررسی قرار گیرد و مشکل را سریعا گردن سرویس دهنده نیندازیم).
برای اینکه بدانیم که سرعت یک هاست چگونه باید ارزیابی شود باید سه مورد را بررسی کنیم :
مورد سوم نزدیکی سرور به کاربران هدف است. هر چقدر traceroute و پینگ زمانی کمتری به سرور داشته باشید، دسترسی به اطلاعات آن سریعتر میشود. در ایران عموما از کشورهایی چون آمریکا، کانادا ، انگلیس ، آلمان و استرالیا سرور خریده میشود و به مدت چندسالی است که در ایران هم این امکان فراهم شده است ولی خب مسائلی که این سرورها در ایران دارند باعث میشوند عدهی زیادی دور آنها را که هیچ روی آنها را هم خط بکشند.
مشکلاتی که این سرورها دارند بیشتر به سه مورد زیر بر میگردد:
هزینهی سنگین ترافیک : این مورد آن قدر به وضوح پیداست که شما را به کل برای هر نوع خریدی پشیمان میکند؛ مگر اینکه پولتان از جایی تامین میشود. ادارات دولتی عموما این نوع هاست را بر میدارند و یا سایتهای اشتراکی که منابع زیادی را مصرف نمیکنند و یا شرکتها یا اشخاصی که پول تامین را دارند.
عدم دسترسی به شبکههای اجتماعی یا هرسرویس دهندهی مسدود شده : من این را تست نکردم ولی با دو دوتا چهارتا کردن این حساب دستم آمد که وقتی سایتی مثل فیس بوک و توئیتر در ایران مسدود باشند، باید روی این سرورها هم مسدود باشند. در نتیجه اگر سایت شما مطالبش را در این سایتها معرفی میکند و میخواهید پست و توئیتی روی آنها داشته باشید، احتمالا توانایی این کار را نخواهید داشت. مگه اینکه خودتان دستی بروید در سایت مربوطه و اضافه کنید.
جایگاه پایینتر SEO : گفتیم که یکی از عوامل سرعت، نزدیک بودن سرور به کاربر هدف است. این مورد برای سرور موتورهای جستجویی مثل گوگل که در ایران سروری ندارند هم اتفاق میافتد و در نتیجه گوگل از لحاظ امتیاز بندی سرعت، امتیاز شما را کم میکند ولی این مورد همیشه چندان صدق نمیکند و مبحث ترافیک سایت بیشتر مورد ارزیابی قرار میگیرد.
امنیت
این مورد شامل دو بخش میشود یکی امنیت دسترسی به سرور و دیگر امنیت نگهداری اطلاعات. نصب فایروالها روی سرور و نظارت 24 ساعته روی سرورهای شرکت (که شامل بخش پشتیبانی میشود) میتواند این موردها را پوشش دهد. هر چند این مورد را زیاد نمیتوانید محک بزنید، ولی اگر اخباری چون «همکاری با یک شرکت امنیتی جهت تست سرور و همکاری با تعدادی هکر جهت تست سرور» و ... را شنیدید، میتوانید این اطمینان را کسب کنید که آنها به این مورد اهمیت میدهند.
امنیت اطلاعات چون پشتیبانهای روزانه و نگهداری اطلاعات تا مدتی معین پس از اتمام قرارداد و موارد این چنینی، میتواند شما را مطمئن سازد. در مورد یکی از مشتریانم به خاطر دارم که فراموش کرده بود هاست را تمدید کند و یک روز بعد از انقضاء ما درخواست دیتابیس را داشتیم که برای ما ارسال کنند و به گفتهی خودشان ما مسئولیتی در قبال نگه داری اطلاعات نداریم، ولی تا 5 روز نگه میداریم که البته گفتند هر چه به دنبال دیتابیس گشتند، چیزی پیدا نکردند که البته این مشکل را از راهکار دیگری حل کردیم و ماجرا به خوشی تمام شد. ولی جالب این بود که اینقدر که من حرص خوردم، چطوری به مدیر اینها بگوئیم که اطلاعات تیمشان پریده و اینها حرص نخوردند و بی خیال.
کنترل پنل
میگویند کنترل پنل هم چیز مهمی است ولی در اکثر اوقات اکثر هاستینگها از همان پنلهای معروف استفاده میکنند. برای مثال در لینوکس "سی پنل" و در ویندوز "وب سایت پنل" و "پلسک" میباشد . تا آخرین باری که من با آنها کار کردم، وب سایت پنل، یک پنل سبک بود که برای انجام عملیات ساده تا متوسط در زمینه کاری پنلها میباشد و پلسک نسبت به آن امکانات خیلی زیادتری دارد. اکثرا آنها حاوی امکانی چون نصب یک کلیکی CMS هایی چون وردپرس و جوملا و ... هستند.
اگر از یک سرور اختصاصی بهره ببرید خودتان مختار نصب هر نوع چیزی روی آن هستید و در مورد کنترل پنل هم صدق میکند ولی عموما شرکتها یک سری الگوهای پیش فرضی را برای سرویسهای خود دارند که در صورت تغییر باید به آنها، تغییرات را طلاع دهید.
فضای دیسک و پهنای باند (ترافیک)
اولین نکاتی که همیشه توسط میزبانها و خریداران مورد توجه قرار میگیرند، این دو مورد هست. در صورتیکه سایت شما اجازه آپلودی به کاربران نمیدهد، یا خودتان هم آپلود چندانی ندارید، میتوانید فضای دیسک سخت پایینتری را انتخاب کنید. مثلا اگر شما تعدادی تصویر دارید و مقدار زیادی فایل متنی و ... به نظر یک گیگ کفایت میکند و در صورتیکه بعدها دیدید این فضا رو به اتمام است، میتوانید به میزبان درخواست افزایش و ارتقاء آن را بدهید؛ اما برای بار اول یک گیگ به خوبی کفایت میکند. ولی اگر چنانچه با فایلهای حجیم سرو کله میزنید و یا تعداد فایلهایی چون تصاویر، صوت و ویدیو در آن زیاد دیده میشود، بهتر است به فکر فضایی بیشتر و حتی گاها unlimited باشید. برای سایتهایی که حجم به شدت بالایی دارند و یا مثلا نیاز به راه اندازی بخش دانلود دارند، بهتر هست از یک هاستینگ به نام هاست دانلود استفاده کنند. این نوع هاستها به شما اجازه میزبانی فایلهای وب سایتهایی چون aspx,mvc,php را نمیدهند و بیشتر پهنای باند و فضای دیسک سخت آن مدنظر است. در این حالت سایت خود را روی یک هاست معمولی به اشتراک میگذارید و فایلهای خود را روی هاست دانلود قرار میدهید و ارتباط آنها را لینک میکنید.
در مورد پهنای باند باید تعداد کاربر و همچنین نوع اطلاعاتی که جابجا میشوند را مورد بررسی قرار دهید. اگر تعداد کاربران پایین است عموما این مقدار کم است و یا اگر اطلاعات متنی فقط جابجا میشود این مقدار کمتر هم میشود.
در سناریوی اول یک انجمن VB را بررسی میکنیم: عموم انجمنها در زمینهی چند رسانهای مثل تصاویر و ...، چیزی جز تصاویر پوسته خود (که کش هم میشوند) را انتقال نمیدهند. به این دلیل که اجازهی آپلود را به کار نمیدهند و کاربرها بیشتر از سرویسهای ثالث برای تصاویر بهره میبرند و به غیر از پوستهی، سایت متون انجمن هستند که متن هم ترافیک پایینی را مصرف میکند. پس در این حالت ترافیک ماهیانهی 10 گیگ میتواند برای شروع کار تا مدتی که سایت بازدیدکنندههای خودش را پیدا کند، مناسب باشد. از نظر دیسک سخت هم به نظر من اگر شما نیاز به قرار دادن تصاویری چون اسلایدشوها و ... را دارید، به انضمام آواتار کاربران، 500 مگابایت میتواند کافی باشد. حجم آواتار کاربران در قسمت مدیریت، کنترل شده است و تنظیم دستی آن میتواند این مقدار حجم آواتارها را افزایش دهد.
در سناریوی دوم ما یک وب سرویس مثلا برای یک برنامهی اندرویدی را مثال میزنیم: عموما وب سرویسها چیزی جز یک فایل متنی را انتقال نمیدهند؛ مگر اینکه از تصاویر، صورت و ویدیو هم بهره برده باشید. در صورتیکه بیشتر همان متن را بخواهید انتقال دهید، ترافیکی جز همان متن مصرف نمیشود و حتی از یک انجمن هم مصرف پایینتری دارد و میتواند ماهیانه 10 گیگ یا حتی کمتر هم مورد استفاده قرار بگیرد. در صورت اضافه شدن تصویر و دیگر موارد چندرسانهای و کاربران در حال افزایش، این مقدار هم به همان تناسب بالا میرود.
سرعت Speed و توان سرویس دهی(Uptime)
بعد از ارزیابی موارد بالا نوبت به این دو مورد میرسد. اینکه هاست شما به قولی چقدر دوام و ایستادگی دارد، بسیار مهم است و در صورتیکه این امر محقق نگردد، میتواند موجب از دست دادن و شنیدن اعتراض کاربرها باشید. uptime به معنی این است که در یک دورهی زمانی چقدر وب سایت شما در دسترس بوده است؛ درست شنیدید! حتما پیش خودتان میگویید خوب، یک وب سایت همیشه در دسترس است. ولی واقعیت را بخواهید خیر، اینگونه نیست. اگر سرویس دهندهی خوبی را انتخاب نکنید، ممکن است در روز یا هفته یا در هرمقطع زمانی، با در دسترس نبودن وب سایت روبرو شوید؛ تقریبا مشابه زمانیکه adsl شما قطع میشود، چند لحظهای (طولانیتر از زمان عادی) مرورگر شما سعی کرده و میبیند که زورش نمیرسد و یک پیام عدم دسترسی را به شما نمایش میدهد.
به خوبی به یاد دارم که یک میزبان، هاستهای ارزان قیمتی را ارائه میکرد، ولی گاها در روز دو الی سه بار پنج دقیقهای سرور از دسترس خارج میشد و میگفتیم این هاست برای کسانی است که پول کافی ندارند و یا خیلی نمیخواهند خرج کنند و حالا پنج دقیقهای هم در روز در دسترس نباشد اهمیتی ندارد. در سایتها بیشتر این مورد را با اعدادی شبیه 99.9% مشخص میکنند و پیشنهاد میکنم هاستی را بخرید که این عدد را به شما نشان میدهد، یا حداقل دیگر 99.5% کمتر نباشد. البته تمامی هاستها همین را مینویسند، ولی واقعیت چیز دیگری است و بهتر از از دوستان و آشنایان و جستجوی در اینترنت و انجمنها به این مورد برسید.
فاکتور بعدی سرعت سرور است و کاربران به این مورد هم اهمیت ویژهای میدهند. به خوبی به یاد دارم، اولین هاستی که در کارم استفاده کردم و به پیشنهاد دوستم که در آن کار میکرد این هاست را خریداری کردم، اوایل خوب بود، ولی مشکل سرعت بعدها اضافه شد که حتی ورود به پنل مدیریتی چند دقیقهای طول میکشید. یا اینکه چند وقت پیش وب سایتی را مطالعه میکردم که فقط متن جابجا میکرد، ولی به طوری که کندتر از زمان اجرای یک سایت با گرافیک متوسط طول میکشید. (البته این نکته حائز اهمیت است خود وب سایت هم باید از این لحاظ مورد بررسی قرار گیرد و مشکل را سریعا گردن سرویس دهنده نیندازیم).
برای اینکه بدانیم که سرعت یک هاست چگونه باید ارزیابی شود باید سه مورد را بررسی کنیم :
- دیتاسنتر
- سرور
- نزدیکی سرور به محل زندگی کاربران هدف
مورد سوم نزدیکی سرور به کاربران هدف است. هر چقدر traceroute و پینگ زمانی کمتری به سرور داشته باشید، دسترسی به اطلاعات آن سریعتر میشود. در ایران عموما از کشورهایی چون آمریکا، کانادا ، انگلیس ، آلمان و استرالیا سرور خریده میشود و به مدت چندسالی است که در ایران هم این امکان فراهم شده است ولی خب مسائلی که این سرورها در ایران دارند باعث میشوند عدهی زیادی دور آنها را که هیچ روی آنها را هم خط بکشند.
مشکلاتی که این سرورها دارند بیشتر به سه مورد زیر بر میگردد:
هزینهی سنگین ترافیک : این مورد آن قدر به وضوح پیداست که شما را به کل برای هر نوع خریدی پشیمان میکند؛ مگر اینکه پولتان از جایی تامین میشود. ادارات دولتی عموما این نوع هاست را بر میدارند و یا سایتهای اشتراکی که منابع زیادی را مصرف نمیکنند و یا شرکتها یا اشخاصی که پول تامین را دارند.
عدم دسترسی به شبکههای اجتماعی یا هرسرویس دهندهی مسدود شده : من این را تست نکردم ولی با دو دوتا چهارتا کردن این حساب دستم آمد که وقتی سایتی مثل فیس بوک و توئیتر در ایران مسدود باشند، باید روی این سرورها هم مسدود باشند. در نتیجه اگر سایت شما مطالبش را در این سایتها معرفی میکند و میخواهید پست و توئیتی روی آنها داشته باشید، احتمالا توانایی این کار را نخواهید داشت. مگه اینکه خودتان دستی بروید در سایت مربوطه و اضافه کنید.
جایگاه پایینتر SEO : گفتیم که یکی از عوامل سرعت، نزدیک بودن سرور به کاربر هدف است. این مورد برای سرور موتورهای جستجویی مثل گوگل که در ایران سروری ندارند هم اتفاق میافتد و در نتیجه گوگل از لحاظ امتیاز بندی سرعت، امتیاز شما را کم میکند ولی این مورد همیشه چندان صدق نمیکند و مبحث ترافیک سایت بیشتر مورد ارزیابی قرار میگیرد.
امنیت
این مورد شامل دو بخش میشود یکی امنیت دسترسی به سرور و دیگر امنیت نگهداری اطلاعات. نصب فایروالها روی سرور و نظارت 24 ساعته روی سرورهای شرکت (که شامل بخش پشتیبانی میشود) میتواند این موردها را پوشش دهد. هر چند این مورد را زیاد نمیتوانید محک بزنید، ولی اگر اخباری چون «همکاری با یک شرکت امنیتی جهت تست سرور و همکاری با تعدادی هکر جهت تست سرور» و ... را شنیدید، میتوانید این اطمینان را کسب کنید که آنها به این مورد اهمیت میدهند.
امنیت اطلاعات چون پشتیبانهای روزانه و نگهداری اطلاعات تا مدتی معین پس از اتمام قرارداد و موارد این چنینی، میتواند شما را مطمئن سازد. در مورد یکی از مشتریانم به خاطر دارم که فراموش کرده بود هاست را تمدید کند و یک روز بعد از انقضاء ما درخواست دیتابیس را داشتیم که برای ما ارسال کنند و به گفتهی خودشان ما مسئولیتی در قبال نگه داری اطلاعات نداریم، ولی تا 5 روز نگه میداریم که البته گفتند هر چه به دنبال دیتابیس گشتند، چیزی پیدا نکردند که البته این مشکل را از راهکار دیگری حل کردیم و ماجرا به خوشی تمام شد. ولی جالب این بود که اینقدر که من حرص خوردم، چطوری به مدیر اینها بگوئیم که اطلاعات تیمشان پریده و اینها حرص نخوردند و بی خیال.
کنترل پنل
میگویند کنترل پنل هم چیز مهمی است ولی در اکثر اوقات اکثر هاستینگها از همان پنلهای معروف استفاده میکنند. برای مثال در لینوکس "سی پنل" و در ویندوز "وب سایت پنل" و "پلسک" میباشد . تا آخرین باری که من با آنها کار کردم، وب سایت پنل، یک پنل سبک بود که برای انجام عملیات ساده تا متوسط در زمینه کاری پنلها میباشد و پلسک نسبت به آن امکانات خیلی زیادتری دارد. اکثرا آنها حاوی امکانی چون نصب یک کلیکی CMS هایی چون وردپرس و جوملا و ... هستند.
اگر از یک سرور اختصاصی بهره ببرید خودتان مختار نصب هر نوع چیزی روی آن هستید و در مورد کنترل پنل هم صدق میکند ولی عموما شرکتها یک سری الگوهای پیش فرضی را برای سرویسهای خود دارند که در صورت تغییر باید به آنها، تغییرات را طلاع دهید.
پشتیبانی فنی
موردی را تصور کنید که ساعت 2 شب هست و سایت شما هم که در زمینهی مهم و حساسی فعالیت میکند، یک دفعه سرویس آن قطع میشود و برای سرور مشکلی پیش میآید. در این موقع چکاری را انجام میدهید؟ صبر میکنید تا صبح شود و سرویس شما راه بیفتد و کاربران هم از سایت شما نا امید شوند؟ اگر این مشکل به دفعات رخ بدهد چه؟ اگر چند روز پشت سرهم تعطیلی باشد چه؟
همهی این موارد مربوط به پشتیبانی میشود. این پشتیبانیها عموما به صورت چت و تلفنی (بیشتر مربوط به ساعات اداری) و ارسال تیکت در سامانه پشتیبانی میشود. البته تجربهی من میگوید در ایران زیاد به متون نوشتهی روی سایت میزبان توجهی نکنید و این را هم به موضوع تحقیق خود اضافه کنید. بعضیها میگویند 24 ساعته پشتیبانی تلفنی دارند ولی هر بار زنگ میزنی کسی پاسخی نمیدهد. یا تیکت میزنید و بارها و بارها پشت سر هم تیکت "چی شد؟" را برای جویا شدن ارسال میکنید. (البته انصاف هم داشته باشید و پنج دقیقه پنج دقیقه این تیکت را نفرستید).
پشتیبانی هر نوع سرور را مطلع شوید. بعضیها واقعا پشتیبانی دارند!؟ ولی وقتی زنگ میزنید میگویند این پشتیبانی مربوط به لینوکس است و بچههای ویندوز فقط ساعات اداری هستند. خلاصه حواستان را جمع کنید که بچههای آن بخش همیشه باشند.
بنابراین مطمئن شوید که یک پشتیبانی 24/7 واقعی داشته باشند.
امکانات اضافه
در کنار خود هاست یک سری ویژگیهایی چون FTP و تعدادی دامنههای پشتیبانی شده و زیر دامنهها و تعداد دیتابیسها و ایمیل و ... هم هستند که در قدیم یادم هست محدودیتهایی در این رابطه ایجاد کرده بودند که امروزه از این نظر در اکثر هاستها آن طور که دیدم این محدودیتها رفع شده است که البته خیلی هم خوب هست و این محدودیتها بیشتر شبیه سودجویی بود تا چیز دیگر.
هزینه
با همهی حرفهای بالا به نظر میرسد که هزینه، همه این معادلات را به هم میریزد. هاستینگهای مختلف و قیمتهای مختلف، تصمیم گیرنده شما هستید که چقدر به عوامل بالا اهمیت میدهید و چگونه آنها را در راستای قیمت اولویت بندی میکنید. اگر محدودیتی ندارید سعی کنید همهی عوامل را بررسی کنید. نکته اینکه اکثر هاستها طرحی به نام ضمانت برگشت پول را در هفت روز یا گاها یک ماه، دارند و در صورتیکه از سرویس خریداری شده راضی نبودید میتوانید پول خود را پس گرفته و سرویس معلق شود.
هاستینگ در قرارداد
هنگام عقد قرارداد با مشتری، در قرارداد در مورد هاستینگ که خودش تهیه میکند، این نکته ذکر شود که شما در مورد مسائلی که مربوط به هاست و دامنه میشود هیچ مسئولیتی ندارید و باید مشکلات را با مسئولان آن در میان بگذارد.
همچنین این نکته هم ذکر شود در مورد مسائلی چون عدم پشتیبانی مناسب هاست، از محصول یا سرویس شما، شما هیچگونه مسئولیتی در قبال آن ندارید و یا باید این مورد توسط شما دنبال شود یا خرید ایشان با مشاوره و تایید شما از هاست مربوطه باشد.
در صورتی که مشتری نمیتواند شخصا خرید هاستینگ را انجام دهد یا در شرکت مسئولین IT ندارند که این کار را انجام دهند و این مورد به شما محول میشود، در قرارداد ذکر شود که شما هیچ گونه مسئولیتی در قبال مشکلاتی که برای هاست و دامنه رخ میدهد ندارید و تنها میتوانید به عنوان یک واسط یا وکیل در ازای مبلغ توافق شدهای مشکل را با مسئولین هاست و دامنه در میان گذاشته و ماجرا را برای حل مشکل ایجاد شده دنبال کنید یا این کار را به عنوان هدیه ای از طرف خدمات طلایی شما به مشتریان به صورت رایگان انجام دهید.
موردی را تصور کنید که ساعت 2 شب هست و سایت شما هم که در زمینهی مهم و حساسی فعالیت میکند، یک دفعه سرویس آن قطع میشود و برای سرور مشکلی پیش میآید. در این موقع چکاری را انجام میدهید؟ صبر میکنید تا صبح شود و سرویس شما راه بیفتد و کاربران هم از سایت شما نا امید شوند؟ اگر این مشکل به دفعات رخ بدهد چه؟ اگر چند روز پشت سرهم تعطیلی باشد چه؟
همهی این موارد مربوط به پشتیبانی میشود. این پشتیبانیها عموما به صورت چت و تلفنی (بیشتر مربوط به ساعات اداری) و ارسال تیکت در سامانه پشتیبانی میشود. البته تجربهی من میگوید در ایران زیاد به متون نوشتهی روی سایت میزبان توجهی نکنید و این را هم به موضوع تحقیق خود اضافه کنید. بعضیها میگویند 24 ساعته پشتیبانی تلفنی دارند ولی هر بار زنگ میزنی کسی پاسخی نمیدهد. یا تیکت میزنید و بارها و بارها پشت سر هم تیکت "چی شد؟" را برای جویا شدن ارسال میکنید. (البته انصاف هم داشته باشید و پنج دقیقه پنج دقیقه این تیکت را نفرستید).
پشتیبانی هر نوع سرور را مطلع شوید. بعضیها واقعا پشتیبانی دارند!؟ ولی وقتی زنگ میزنید میگویند این پشتیبانی مربوط به لینوکس است و بچههای ویندوز فقط ساعات اداری هستند. خلاصه حواستان را جمع کنید که بچههای آن بخش همیشه باشند.
بنابراین مطمئن شوید که یک پشتیبانی 24/7 واقعی داشته باشند.
امکانات اضافه
در کنار خود هاست یک سری ویژگیهایی چون FTP و تعدادی دامنههای پشتیبانی شده و زیر دامنهها و تعداد دیتابیسها و ایمیل و ... هم هستند که در قدیم یادم هست محدودیتهایی در این رابطه ایجاد کرده بودند که امروزه از این نظر در اکثر هاستها آن طور که دیدم این محدودیتها رفع شده است که البته خیلی هم خوب هست و این محدودیتها بیشتر شبیه سودجویی بود تا چیز دیگر.
هزینه
با همهی حرفهای بالا به نظر میرسد که هزینه، همه این معادلات را به هم میریزد. هاستینگهای مختلف و قیمتهای مختلف، تصمیم گیرنده شما هستید که چقدر به عوامل بالا اهمیت میدهید و چگونه آنها را در راستای قیمت اولویت بندی میکنید. اگر محدودیتی ندارید سعی کنید همهی عوامل را بررسی کنید. نکته اینکه اکثر هاستها طرحی به نام ضمانت برگشت پول را در هفت روز یا گاها یک ماه، دارند و در صورتیکه از سرویس خریداری شده راضی نبودید میتوانید پول خود را پس گرفته و سرویس معلق شود.
کل مطالب گفته شده در چک لیست به طور خلاصه : اول از همه بررسی فضای ذخیره سازی و پهنای باند، بعد از آن بررسی توانایی سرویس دهی و سرعت سرورها، داشتن محیط امن و پشتیبانی مناسب بود.
هاستینگ در قرارداد
هنگام عقد قرارداد با مشتری، در قرارداد در مورد هاستینگ که خودش تهیه میکند، این نکته ذکر شود که شما در مورد مسائلی که مربوط به هاست و دامنه میشود هیچ مسئولیتی ندارید و باید مشکلات را با مسئولان آن در میان بگذارد.
همچنین این نکته هم ذکر شود در مورد مسائلی چون عدم پشتیبانی مناسب هاست، از محصول یا سرویس شما، شما هیچگونه مسئولیتی در قبال آن ندارید و یا باید این مورد توسط شما دنبال شود یا خرید ایشان با مشاوره و تایید شما از هاست مربوطه باشد.
در صورتی که مشتری نمیتواند شخصا خرید هاستینگ را انجام دهد یا در شرکت مسئولین IT ندارند که این کار را انجام دهند و این مورد به شما محول میشود، در قرارداد ذکر شود که شما هیچ گونه مسئولیتی در قبال مشکلاتی که برای هاست و دامنه رخ میدهد ندارید و تنها میتوانید به عنوان یک واسط یا وکیل در ازای مبلغ توافق شدهای مشکل را با مسئولین هاست و دامنه در میان گذاشته و ماجرا را برای حل مشکل ایجاد شده دنبال کنید یا این کار را به عنوان هدیه ای از طرف خدمات طلایی شما به مشتریان به صورت رایگان انجام دهید.
API کار با کوکیها نیز در ASP.NET Core نسبت به نگارشهای دیگر تغییریافتهاست که در ادامه این موارد را بررسی خواهیم کرد. همچنین با کمک مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» یک تامین کنندهی سفارشی کوکیهای رمزنگاری شده را نیز ایجاد میکنیم.
خلاصهای از روشهای کار با کوکیها در ASP.NET Core
ایجاد یک کوکی جدید
کوکی جدید را میتوان توسط متد Append مجموعهی کوکیها، به Response اضافه کرد:
همانطور که در تصویر نیز مشخص است، طول عمر این کوکی، به سشن تنظیم شدهاست و با پایان سشن جاری مرورگر (بسته شدن کل مرورگر)، این کوکی نیز غیرمعتبر شده و به صورت خودکار حذف خواهد شد. برای تعیین عمر دقیق یک کوکی میتوان از خاصیت Expires شیء CookieOptions که در مثال فوق مقدار دهی نشدهاست، استفاده کرد؛ مانند:
خواندن محتویات کوکی ذخیره شده
پس از ثبت کوکی در Response، خواندن آن در Request بعدی به شکل زیر است:
در این حالت اگر کلید درخواستی در مجموعهی کوکیها یافت نشد، نال بازگشت داده میشود.
شیء this.Request.Cookies از نوع IRequestCookieCollection است:
و همانطور که ملاحظه میکنید، برای دریافت مقدار یک کوکی یا میتوان از indexer آن مانند مثال فوق و یا از متد TryGetValue استفاده کرد.
در مستندات آن عنوان شدهاست که در حالت استفادهی از indexer، درصورت یافت نشدن کلید، string.Empty بازگشت داده میشود (که آزمایشات null را نمایش میدهند). اما در حالت استفادهی از TryGetValue بر اساس خروجی bool آن دقیقا میتوان مشخص کرد که آیا این کوکی وجود داشتهاست یا خیر.
در اینجا همچنین متد ContainsKey نیز جهت بررسی وجود یک کلید، در مجموعهی کلیدها نیز پیش بینی شدهاست.
بنابراین بهتر است جهت یافتن مقادیر کوکیها از روش ذیل استفاده کرد:
حذف کوکیهای موجود
در اینجا متد Delete نیز پیش بینی شدهاست که باید بر روی کوکیهای Response فراخوانی شود:
کار آن افزودن یک کوکی دیگر با همین کلید، اما منقضی شدهاست؛ تا مرورگر را مجبور به حذف آن کند. در اینجا در صورت نیاز میتوان به عنوان پارامتر دوم، CookieOptions این کوکی جدید را نیز تنظیم کرد.
همانطور که در تصویر نیز مشخص است، در صورت عدم تنظیم CookieOptions، این کوکی جدید اضافه شده، دارای تاریخ انقضای 1970 است که سبب خواهد شد تا توسط مرورگر، غیرمعتبر درنظر گرفته شده و حذف شود.
طراحی یک تامین کنندهی کوکیهای امن
پس از آشنایی با مقدمات کوکیها و همچنین «بررسی تغییرات رمزنگاری اطلاعات در NET Core.»، اکنون میتوان یک تامین کنندهی کوکیهای رمزنگاری شده را برای ASP.NET Core به نحو ذیل طراحی کرد:
- نکاتی را که در ابتدای مطلب در مورد ثبت و خواندن و حذف کوکیها مطالعه کردید، به این کلاس اضافه شدهاند. با این تغییر که پیش از ذخیرهی مقدار کوکی، این مقدار رمزنگاری میشود و همچنین پس از خواندن آن، رمزگشایی خواهد شد.
- در این تامین کنندهی کوکیهای امن، IProtectionProvider تزریقی به سازندهی کلاس را در مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» پیشتر ملاحظه کردهاید.
- در اینجا برای ثبت سرویس جدید، تنظیمات ابتدایی برنامه چنین شکلی را پیدا میکنند و پس از آن میتوان سرویس ISecureCookiesProvider را به کنترلرهای برنامه تزریق و استفاده کرد:
- چون در کلاس SecureCookiesProvider، خاصیت Expires تنظیم نشدهاست، طول عمر این کوکیها محدود است به مدت زمان باز بودن مرورگر. بنابراین در صورت نیاز این مورد را تغییر دهید.
خلاصهای از روشهای کار با کوکیها در ASP.NET Core
ایجاد یک کوکی جدید
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestCookiesController : Controller { public IActionResult Index() { this.Response.Cookies.Append("key", "value", new CookieOptions { HttpOnly = true, Path = this.Request.PathBase.HasValue ? this.Request.PathBase.ToString() : "/", Secure = this.Request.IsHttps }); return Content("OK!"); } } }
همانطور که در تصویر نیز مشخص است، طول عمر این کوکی، به سشن تنظیم شدهاست و با پایان سشن جاری مرورگر (بسته شدن کل مرورگر)، این کوکی نیز غیرمعتبر شده و به صورت خودکار حذف خواهد شد. برای تعیین عمر دقیق یک کوکی میتوان از خاصیت Expires شیء CookieOptions که در مثال فوق مقدار دهی نشدهاست، استفاده کرد؛ مانند:
Expires = DateTimeOffset.UtcNow.AddDays(2)
خواندن محتویات کوکی ذخیره شده
پس از ثبت کوکی در Response، خواندن آن در Request بعدی به شکل زیر است:
var value = this.Request.Cookies["key"];
شیء this.Request.Cookies از نوع IRequestCookieCollection است:
public interface IRequestCookieCollection : IEnumerable<KeyValuePair<string, string>>, IEnumerable { string this[string key] { get; } ICollection<string> Keys { get; } bool ContainsKey(string key); bool TryGetValue(string key, out string value); }
در مستندات آن عنوان شدهاست که در حالت استفادهی از indexer، درصورت یافت نشدن کلید، string.Empty بازگشت داده میشود (که آزمایشات null را نمایش میدهند). اما در حالت استفادهی از TryGetValue بر اساس خروجی bool آن دقیقا میتوان مشخص کرد که آیا این کوکی وجود داشتهاست یا خیر.
در اینجا همچنین متد ContainsKey نیز جهت بررسی وجود یک کلید، در مجموعهی کلیدها نیز پیش بینی شدهاست.
بنابراین بهتر است جهت یافتن مقادیر کوکیها از روش ذیل استفاده کرد:
string cookieValue; if (this.Request.Cookies.TryGetValue(key, out cookieValue)) { // TODO: use the cookieValue } else { // this cookie doesn't exist. }
حذف کوکیهای موجود
در اینجا متد Delete نیز پیش بینی شدهاست که باید بر روی کوکیهای Response فراخوانی شود:
this.Response.Cookies.Delete("key");
همانطور که در تصویر نیز مشخص است، در صورت عدم تنظیم CookieOptions، این کوکی جدید اضافه شده، دارای تاریخ انقضای 1970 است که سبب خواهد شد تا توسط مرورگر، غیرمعتبر درنظر گرفته شده و حذف شود.
طراحی یک تامین کنندهی کوکیهای امن
پس از آشنایی با مقدمات کوکیها و همچنین «بررسی تغییرات رمزنگاری اطلاعات در NET Core.»، اکنون میتوان یک تامین کنندهی کوکیهای رمزنگاری شده را برای ASP.NET Core به نحو ذیل طراحی کرد:
public interface ISecureCookiesProvider { void Add(HttpContext context, string token, string value); bool Contains(HttpContext context, string token); string GetValue(HttpContext context, string token); void Remove(HttpContext context, string token); } public class SecureCookiesProvider : ISecureCookiesProvider { private readonly IProtectionProvider _protectionProvider; public SecureCookiesProvider(IProtectionProvider protectionProvider) { _protectionProvider = protectionProvider; } public void Add(HttpContext context, string token, string value) { value = _protectionProvider.Encrypt(value); context.Response.Cookies.Append(token, value, getCookieOptions(context)); } public bool Contains(HttpContext context, string token) { return context.Request.Cookies.ContainsKey(token); } public string GetValue(HttpContext context, string token) { string cookieValue; if (!context.Request.Cookies.TryGetValue(token, out cookieValue)) { return null; } return _protectionProvider.Decrypt(cookieValue); } public void Remove(HttpContext context, string token) { if (context.Request.Cookies.ContainsKey(token)) { context.Response.Cookies.Delete(token); } } /// <summary> /// Expires at the end of the browser's session. /// </summary> private CookieOptions getCookieOptions(HttpContext context) { return new CookieOptions { HttpOnly = true, Path = context.Request.PathBase.HasValue ? context.Request.PathBase.ToString() : "/", Secure = context.Request.IsHttps }; } }
- در این تامین کنندهی کوکیهای امن، IProtectionProvider تزریقی به سازندهی کلاس را در مطلب «تغییرات رمزنگاری اطلاعات در NET Core.» پیشتر ملاحظه کردهاید.
- در اینجا برای ثبت سرویس جدید، تنظیمات ابتدایی برنامه چنین شکلی را پیدا میکنند و پس از آن میتوان سرویس ISecureCookiesProvider را به کنترلرهای برنامه تزریق و استفاده کرد:
public class Startup { public void ConfigureServices(IServiceCollection services) { services.TryAddSingleton<IProtectionProvider, ProtectionProvider>(); services.TryAddSingleton<ISecureCookiesProvider, SecureCookiesProvider>();
مطالب
ASP.NET MVC #20
تهیه گزارشات تحت وب به کمک WebGrid
WebGrid از ASP.NET MVC 3.0 به صورت توکار به شکل یک Html Helper در دسترس میباشد و هدف از آن سادهتر سازی تهیه گزارشات تحت وب است. البته این گرید، تنها گرید مهیای مخصوص ASP.NET MVC نیست و پروژه MVC Contrib یا شرکت Telerik نیز نمونههای دیگری را ارائه دادهاند؛ اما از این جهت که این Html Helper، بدون نیاز به کتابخانههای جانبی در دسترس است، بررسی آن ضروری میباشد.
صورت مساله
لیستی از کارمندان به همراه حقوق ماهیانه آنها در دست است. اکنون نیاز به گزارشی تحت وب، با مشخصات زیر میباشد:
1- گزارش باید دارای صفحه بندی بوده و هر صفحه تنها 10 ردیف را نمایش دهد.
2- سطرها باید یک در میان دارای رنگی متفاوت باشند.
3- ستون حقوق کارمندان در پایین هر صفحه، باید دارای جمع باشد.
4- بتوان با کلیک بر روی عنوان هر ستون، اطلاعات را بر اساس ستون انتخابی، مرتب ساخت.
5- لینکهای حذف یا ویرایش یک ردیف نیز در این گزارش مهیا باشد.
6- لیست تهیه شده، دارای ستونی به نام «ردیف» نیست. این ستون را نیز به صورت خودکار اضافه کنید.
7- لیست نهایی اطلاعات، دارای ستونی به نام مالیات نیست. فقط حقوق کارمندان ذکر شده است. ستون محاسبه شده مالیات نیز باید به صورت خودکار در این گزارش نمایش داده شود. این ستون نیز باید دارای جمع پایین هر صفحه باشد.
8- تمام اعداد این گزارش در حین نمایش باید دارای جدا کننده سه رقمی باشند.
9- تاریخهای موجود در لیست، میلادی هستند. نیاز است این تاریخها در حین نمایش شمسی شوند.
10- انتهای هر صفحه گزارش باید بتوان برچسب «صفحه y/n» را مشاهده کرد. n در اینجا منظور تعداد کل صفحات است و y شماره صفحه جاری میباشد.
11- انتهای هر صفحه گزارش باید بتوان برچسب «رکوردهای y تا x از n» را مشاهده کرد. n در اینجا منظور تعداد کل رکوردها است.
12- نام کوچک هر کارمند، ضخیم نمایش داده شود.
13- به ازای هر شماره کارمندی، یک تصویر در پوشه images سایت وجود دارد. برای مثال images/id.jpg. ستونی برای نمایش تصویر متناظر با هر کارمند نیز باید اضافه شود.
14- به ازای هر کارمند، تعدادی پروژه هم وجود دارد. پروژههای متناظر را توسط یک گرید تو در تو نمایش دهید.
راه حل به کمک استفاده از WebGrid
ابتدا یک پروژه خالی ASP.NET MVC را آغاز کنید. سپس مدلهای زیر را به آن اضافه نمائید (یک کارمند که میتواند تعداد پروژه منتسب داشته باشد):
using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime AddDate { get; set; }
public double Salary { get; set; }
public IList<Project> Projects { get; set; }
}
}
namespace MvcApplication17.Models
{
public class Project
{
public int Id { set; get; }
public string Name { set; get; }
}
}
سپس منبع داده نمونه زیر را به پروژه اضافه کنید. به عمد از ORM خاصی استفاده نشده تا بتوانید پروژه جاری را به سادگی در یک پروژه آزمایشی جدید، تکرار کنید.
using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
var rnd = new Random();
for (int i = 1; i <= 1000; i++)
{
list.Add(new Employee
{
Id = i + 1000,
FirstName = "fName " + i,
LastName = "lName " + i,
AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
Salary = rnd.Next(400, 3000),
Projects = CreateRandomProjects()
});
}
return list;
}
private static IList<Project> CreateRandomProjects()
{
var list = new List<Project>();
var rnd = new Random();
for (int i = 0; i < rnd.Next(1, 7); i++)
{
list.Add(new Project
{
Id = i,
Name = "Project " + i
});
}
return list;
}
}
}
در ادامه یک کنترلر جدید را با محتوای زیر اضافه نمائید:
using System.Web.Mvc;
using MvcApplication17.Models;
namespace MvcApplication17.Controllers
{
public class HomeController : Controller
{
[HttpPost]
public ActionResult Delete(int? id)
{
return RedirectToAction("Index");
}
[HttpGet]
public ActionResult Edit(int? id)
{
return View();
}
[HttpGet]
public ActionResult Index(string sort, string sortdir, int? page = 1)
{
var list = EmployeeDataSource.CreateEmployees();
return View(list);
}
}
}
علت تعریف متد index با پارامترهای sort و غیره به URLهای خودکاری از نوع زیر بر میگردد:
http://localhost:3034/?sort=LastName&sortdir=ASC&page=3
همانطور که ملاحظه میکنید، گرید رندر شده، از یک سری کوئری استرینگ برای مشخص سازی صفحه جاری، یا جهت مرتب سازی (صعودی و نزولی بودن آن) یا فیلد پیش فرض مرتب سازی، کمک میگیرد.
سپس یک View خالی را نیز برای متد Index ایجاد کنید. تا اینجا تنظیمات اولیه پروژه انجام شد.
کدهای کامل View را در ادامه ملاحظه میکنید:
@using System.Globalization
@model IList<MvcApplication17.Models.Employee>
@{
ViewBag.Title = "Index";
}
@helper WebGridPageFirstItem(WebGrid grid)
{
@(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}
@helper WebGridPageLastItem(WebGrid grid)
{
if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
{
@grid.TotalRowCount;
}
else
{
@((grid.PageIndex + 1) * grid.RowsPerPage);
}
}
<h2>Employees List</h2>
@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
<div id="container">
@grid.GetHtml(
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
mode: WebGridPagerModes.All,
columns: grid.Columns(
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
)
)
<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
@*
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
*@
</div>
@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
توضیحات ریز جزئیات View فوق
تعریف ابتدایی شیء WebGrid و مقدار دهی آن
در ابتدا نیاز است یک وهله از شیء WebGrid را ایجاد کنیم. در اینجا میتوان تنظیم کرد که آیا نیاز است اطلاعات نمایش داده شده دارای صفحه بندی (canPage) خودکار باشند؟ منبع داده (source) کدام است. در صورت فعال سازی صفحه بندی خودکار، چه تعداد ردیف (rowsPerPage) در هر صفحه نمایش داده شود. آیا نیاز است بتوان با کلیک بر روی سر ستونها، اطلاعات را بر اساس فیلد متناظر با آن مرتب (canSort) ساخت؟ همچنین در صورت نیاز به مرتب سازی، اولین باری که گرید نمایش داده میشود، بر اساس چه فیلدی (defaultSort) باید مرتب شده نمایش داده شود:
@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
در اینجا همچنین سه متغیر کمکی هم تعریف شده که از اینها برای تهیه جمع ستونهای حقوق و مالیات و همچنین نمایش شماره ردیف جاری استفاده میشود. فرمول نحوه محاسبه اولین ردیف هر صفحه را هم ملاحظه میکنید. شماره ردیفهای بعدی، rowIndex++ خواهند بود.
تعریف رنگ و لعاب گرید نمایش داده شده
در ادامه به کمک متد grid.GetHtml، رشتهای معادل اطلاعات HTML صفحه جاری، بازگشت داده میشود. در اینجا میتوان یک سری خواص تکمیلی را تنظیم نمود. برای مثال:
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
هر کدام از این رشتهها در حین رندر نهایی گرید، تبدیل به یک class خواهند شد. برای نمونه:
<div id="container">
<table class="webgrid" id="MyGrid">
<thead>
<tr class="webgrid-header">
به این ترتیب با اندکی ویرایش css سایت، میتوان انواع و اقسام رنگها را به سطرها و ستونهای گرید نهایی اعمال کرد. برای مثال اطلاعات زیر را به فایل css سایت اضافه نمائید:
/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
width: 100%;
margin: 0px;
padding: 0px;
border: 0px;
border-collapse: collapse;
font-family: Tahoma;
font-size: 9pt;
}
.webgrid a
{
color: #000;
}
.webgrid-header
{
padding: 0px 5px;
text-align: center;
border-bottom: 2px solid #739ace;
height: 20px;
border-top: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-header th
{
background-color: #eaf0ff;
border-right: 1px solid #ddd;
}
.webgrid-footer
{
padding: 6px 5px;
text-align: center;
background-color: #e8eef4;
border-top: 2px solid #3966A2;
height: 25px;
border-bottom: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-alternating-row
{
height: 22px;
background-color: #f2f2f2;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-row-style
{
height: 22px;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-selected-row
{
font-weight: bold;
}
.text-align-center-col
{
text-align: center;
}
.total-row
{
background-color:#f9eef4;
}
همانطور که ملاحظه میکنید، رنگهای ردیفها، هدر و فوتر گرید و غیره در اینجا تنظیم میشوند.
به علاوه اگر دقت کرده باشید در تعاریف گرید، htmlAttributes هم مقدار دهی شده است. در اینجا به کمک یک anonymously typed object، مقدار id گرید مشخص شده است. از این id در حین کار با jQuery استفاده خواهیم کرد.
تعیین نوع Pager
پارامتر دیگری که در متد grid.GetHtml تنظیم شده است، mode: WebGridPagerModes.All میباشد. WebGridPagerModes یک enum با محتوای زیر است و توسط آن میتوان نوع Pager گرید را تعیین کرد:
[Flags]
public enum WebGridPagerModes
{
Numeric = 1,
//
NextPrevious = 2,
//
FirstLast = 4,
//
All = 7,
}
نحوه تعریف ستونهای گرید
اکنون به مهمترین قسمت تهیه گزارش رسیدهایم. در اینجا با مقدار دهی پارامتر columns، نحوه نمایش اطلاعات ستونهای مختلف مشخص میگردد. مقداری که باید در اینجا تنظیم شود، آرایهای از نوع WebGridColumn میباشد و مرسوم است به کمک متد کمکی grid.Columns، اینکار را انجام داد.
متد کمکی grid.Column، یک وهله از شیء WebGridColumn را بر میگرداند و از آن برای تعریف هر ستون استفاده خواهیم کرد. توسط پارامتر columnName آن، نام فیلدی که باید اطلاعات ستون جاری از آن اخذ شود مشخص میشود. به کمک پارامتر header، عبارت سرستون متناظر تنظیم میگردد. پارامتر format، مهمترین و توانمندترین پارامتر متد grid.Column است:
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
پارامتر format، به نحو زیر تعریف شده است:
Func<dynamic, object> format
به این معنا که هر بار پیش از رندر سطر جاری، زمانیکه قرار است سلولی رندر شود، یک شیء dynamic در اختیار شما قرار میگیرد. این شیء dynamic یک رکورد از اطلاعات Model جاری است. به این ترتیب به اطلاعات تمام سلولهای ردیف جاری دسترسی خواهیم داشت. بر این اساس هر نوع پردازشی را که لازم بود، انجام دهید (شبیه به فرمول نویسی در ابزارهای گزارش سازی، اما اینبار با کدهای سی شارپ) و مقدار فرمت شده نهایی را به صورت یک رشته بر گردانید. این رشته نهایتا در سلول جاری درج خواهد شد.
اگر از پارامتر فرمت استفاده نشود، همان مقدار فیلد جاری بدون تغییری رندر میگردد.
حداقل به دو نحو میتوان پارامتر فرمت را مقدار دهی کرد:
format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
}
مستقیما از توانمندیهای Razor استفاده کنید. مثلا یک تگ کامل را بدون نیاز به محصور سازی آن بین "" شروع کنید. سپس @item به وهلهای از رکورد در دسترس اشاره میکند که در اینجا وهلهای از شیء کارمند است.
و یا همانند روشی که برای محاسبه جمع حقوق هر صفحه مشاهده میکنید، مستقیما از lambda expressions برای تعریف یک anonymous delegate استفاده کنید.
نحوه اضافه کردن ستون ردیف
ستون ردیف، یک ستون محاسبه شده (calculated field) است:
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
نیازی نیست حتما یک grid.Column، به فیلدی در کلاس کارمند اشاره کند. مقدار سفارشی آن را به کمک پارامتر format تعیین خواهیم کرد. هر بار که قرار است یک ردیف رندر شود، یکبار این پارامتر فراخوانی خواهد شد. فرمول محاسبه rowIndex ابتدای صفحه را نیز پیشتر ملاحظه نمودید.
نحوه اضافه کردن ستون سفارشی تصاویر کارمندها
ستون تصویر کارمندها نیز مستقیما در کلاس کارمند تعریف نشده است. بنابراین میتوان آنرا با مقدار دهی صحیح پارامتر format ایجاد کرد:
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
در این مثال، تصاویر کارمندها در پوشه images واقع در ریشه سایت، قرار دارند. به همین جهت از متد Url.Content برای مقدار دهی صحیح آن استفاده کردیم. به علاوه در اینجا @item.Id به Id رکورد در حال رندر اشاره میکند.
نحوه تبدیل تاریخها به تاریخ شمسی
در ادامه بازهم به کمک پارامتر format، یک وهله از شیء dynamic اشاره کننده به رکورد در حال رندر را دریافت میکنیم. سپس فرصت خواهیم داشت تا بر این اساس، فرمول نویسی کنیم. دست آخر هم رشته مورد نظر نهایی را بازگشت میدهیم:
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
اضافه کردن ستون سفارشی مالیات
در کلاس کارمند، خاصیت حقوق وجود دارد اما مالیات خیر. با توجه به آن میتوانیم به کمک پارامتر format، به اطلاعات شیء dynamic در حال رندر دسترسی داشته باشیم. بنابراین به اطلاعات حقوق دسترسی داریم و سپس با کمی فرمول نویسی، مقدار نهایی مورد نظر را بازگشت خواهیم داد. همچنین در اینجا میتوان نحوه بازگشت مقدار حقوق را به صورت رشتهای حاوی جدا کنندههای سه رقمی نیز مشاهده کرد:
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
اضافه کردن گردیدهای تو در تو
متد Grid.GetHtml، یک رشته را بر میگرداند. بنابراین در هر چند سطح که نیاز باشد میتوان یک گرید را بر اساس اطلاعات دردسترس رندر کرد و سپس بازگشت داد:
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
در اینجا کار اصلی از طریق پارامتر format شروع میشود. سپس به کمک item.Projects به لیست پروژههای هر کارمند دسترسی خواهیم داشت. بر این اساس یک گرید جدید را تولید کرد و سپس رشته معادل با آن را به کمک متد subGrid.GetHtml دریافت و بازگشت میدهیم. این رشته در سلول جاری درج خواهد شد. به نوعی یک گزارش master detail یا sub report را تولید کردهایم.
اضافه کردن دکمههای ویرایش، حذف و انتخاب
هر سه دکمه ویرایش، حذف و انتخاب در ستونهایی سفارشی قرار خواهند گرفت. بنابراین مقدار دهی header و format متد grid.Column کفایت میکند:
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
نکته جدیدی که در اینجا وجود دارد متد item.GetSelectLink میباشد. این متد جزو متدهای توکار گرید است و کار آن بازگشت دادن شیء grid.SelectedRow میباشد. این شیء پویا، حاوی اطلاعات رکورد انتخاب شده است. برای مثال اگر نیاز باشد این اطلاعات به صفحهای ارسال شود، میتوان از روش زیر استفاده کرد:
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
نمایش برچسبهای صفحه x از n و رکوردهای x تا y از z
در یک گزارش خوب باید مشخص باشد که صفحه جاری، کدامین صفحه از چه تعداد صفحه کلی است. یا رکوردهای صفحه جاری چه بازهای از تعداد رکوردهای کلی را تشکیل میدهند. برای این منظور چند متد کمکی به نامهای WebGridPageFirstItem و WebGridPageLastItem تهیه شدهاند که آنها را در ابتدای View ارائه شده، مشاهده نمودید:
<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
نمایش جمع ستونهای حقوق و مالیات در هر صفحه
گرید توکار همراه با ASP.NET MVC در این مورد راه حلی را ارائه نمیدهد. بنابراین باید اندکی دست به ابتکار زد. مثلا:
@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
در این مثال به کمک jQuery با توجه به اینکه id گرید ما MyGrid است، یک ردیف سفارشی که همان جمع محاسبه شده است، به tbody جدول نهایی تولیدی اضافه میشود. از tbody:first هم در اینجا استفاده شده است تا ردیف اضافه شده به گریدهای تو در تو اعمال نشود.
سپس فایل Views\Shared\_Layout.cshtml را گشوده و از section تعریف شده، برای مقدار دهی master page سایت، استفاده نمائید:
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
@RenderSection("script", required: false)
</head>
طی این پست
با تزریق وابستگیها در Asp.net MVC آشنا شدید. روش ذکر شده در آن برای کنترلرهای Web Api جوابگو نیست و باید از روشهای دیگری
برای این منظور استفاده نماییم.
نکته 1: برای پیاده سازی این مثالها، Castle Windsor به عنوان IOC Container انتخاب شده است. بدیهی است میتوانید از Ioc Container مورد نظر خود نیز بهره ببرید.
نکته 2: میتوانید از مقاله [هاست سرویسهای Web Api با استفاده از OWIN و TopShelf] جهت هاست سرویسهای web Api خود استفاده نمایید.
نکته 1: برای پیاده سازی این مثالها، Castle Windsor به عنوان IOC Container انتخاب شده است. بدیهی است میتوانید از Ioc Container مورد نظر خود نیز بهره ببرید.
نکته 2: میتوانید از مقاله [هاست سرویسهای Web Api با استفاده از OWIN و TopShelf] جهت هاست سرویسهای web Api خود استفاده نمایید.
روش اول
اگر قبلا در این زمینه جستجو کرده باشید، به احتمال
زیاد با مفهوم IDependencyResolver بیگانه نیستید. درباره استفاده از این
روش مقالات متعددی نوشته شده است؛ حتی در مثالهای موجود در خود سایت MSDN
نیز این روش را مرسوم دانسته و آن را به اشتراک میگذارند. جهت نمونه
میتوانید این پروژه را دانلود کرده و کدهای آن را بررسی کنید.
در این روش، قدم اول، ساخت یک کلاس و پیاده سازی اینترفیس IDependencyResolver میباشد؛ به صورت زیر:
اینترفیس IDependencyResolver از اینترفیس دیگری به نام
IDependencyScope ارث میبرد که دارای دو متد اصلی به نامهای GetService و
GetServices است که جهت وهله سازی کنترلرها استفاده میشوند. با فراخوانی
این متدها، نمونهی ساخته شده توسط Container بازگشت داده خواهد شد.
کاربرد متد BeginScope چیست؟
کنترلرها به صورت (Per Request) بر اساس هر درخواست وهله سازی خواهند شد. جهت مدیریت چرخهی عمر کنترلرها و منابع در اختیار آنها، از متد BeginScope استفاده میشود. به این صورت که نمونهی اصلی DependencyResolver در هنگام شروع برنامه به GlobalConfiguration پروژه Attach خواهد شد. سپس به ازای هر درخواست، جهت وهله سازی Controllerها، متد GetService از محدوده داخلی (منظور فراخوانی متد BeginScope است) باعث ایجاد نمونه و بعد از اتمام فرآیند، متد Dispose باعث آزاد سازی منابع موجود خواهد شد.
پیاده سازی متد BeginScope وابسته به IocContainer مورد استفاده شما است. در این جا کلاس SharedDependencyResolver را به صورت زیر پیاده سازی کردم:
اگر از UnityContainer استفاده میکنید کافیست تکه کد زیر را جایگزین کلاس بالا نمایید:
برای جستجوی خودکار کنترلرها و رجیستر کردن آنها به برنامه
Windsor امکانات جالبی را در اختیار ما قرار میدهد. ابتدا یک Installer ایجاد میکنیم:
در پایان در کلاس Startup نیز کافیست مراحل زیر را انجام دهید:
»ابتدا Installer نوشته شده را به WindsorContainer معرفی نمایید.
»DependencyResolver نوشته شده را به HttpConfiguration معرفی کنید.
»عملیات Routing مورد نظر را ایجاد و سپس config مورد نظر را در اختیار appBuilder قرار دهید.
نکته: این روش به دلیل استفاده از الگوی ServiceLocator و همچنین نداشتن Context درخواست ها روشی منسوخ شده میباشد که طی این مقاله جناب نصیری به صورت کامل به این مبحث پرداخته اند.
در این روش، قدم اول، ساخت یک کلاس و پیاده سازی اینترفیس IDependencyResolver میباشد؛ به صورت زیر:
public class ApiDependencyResolver : IDependencyResolver { public ApiDependencyResolver(IWindsorContainer container) { Container = container; } public IWindsorContainer Container { get; private set; } public object GetService(Type serviceType) { try { return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null; } catch (Kernel.ComponentNotFoundException) { return null; } } public IEnumerable<object> GetServices(Type serviceType) { try { return Container.ResolveAll(serviceType).Cast<object>(); } catch (Kernel.ComponentNotFoundException) { return Enumerable.Empty<object>(); } } public IDependencyScope BeginScope() { return new SharedDependencyResolver(Container); } public void Dispose() { Container.Dispose(); } }
کاربرد متد BeginScope چیست؟
کنترلرها به صورت (Per Request) بر اساس هر درخواست وهله سازی خواهند شد. جهت مدیریت چرخهی عمر کنترلرها و منابع در اختیار آنها، از متد BeginScope استفاده میشود. به این صورت که نمونهی اصلی DependencyResolver در هنگام شروع برنامه به GlobalConfiguration پروژه Attach خواهد شد. سپس به ازای هر درخواست، جهت وهله سازی Controllerها، متد GetService از محدوده داخلی (منظور فراخوانی متد BeginScope است) باعث ایجاد نمونه و بعد از اتمام فرآیند، متد Dispose باعث آزاد سازی منابع موجود خواهد شد.
پیاده سازی متد BeginScope وابسته به IocContainer مورد استفاده شما است. در این جا کلاس SharedDependencyResolver را به صورت زیر پیاده سازی کردم:
public class SharedDependencyResolver : IDependencyScope { public SharedDependencyResolver(IWindsorContainer container) { Container = container; Scope = Container.BeginScope(); } public IWindsorContainer Container { get; private set; } public IDisposable Scope { get; private set; } public object GetService(Type serviceType) { try { return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null; } catch (ComponentNotFoundException) { return null; } } public IEnumerable<object> GetServices(Type serviceType) { try { return Container.ResolveAll(serviceType).Cast<object>(); } catch (ComponentNotFoundException) { return null; } } public void Dispose() { Scope.Dispose(); } }
public IDependencyScope BeginScope() { var child = container.CreateChildContainer(); return new ApiDependencyResolver(child); }
public class KernelInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register(Classes.FromThisAssembly().BasedOn<ApiController>().LifestyleTransient()); container.Kernel.Resolver.AddSubResolver(new CollectionResolver(container.Kernel, true)); } }
در پایان در کلاس Startup نیز کافیست مراحل زیر را انجام دهید:
»ابتدا Installer نوشته شده را به WindsorContainer معرفی نمایید.
»DependencyResolver نوشته شده را به HttpConfiguration معرفی کنید.
»عملیات Routing مورد نظر را ایجاد و سپس config مورد نظر را در اختیار appBuilder قرار دهید.
public class Startup { public void Configuration( IAppBuilder appBuilder ) { var container = new WindsorContainer(); container.Install(new KernelInstaller()); var config = new HttpConfiguration { DependencyResolver = new ApiDependencyResolver(container) }; config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "Default" , routeTemplate: "{controller}/{action}/{name}" , defaults: new { name = RouteParameter.Optional } ); config.EnsureInitialized(); appBuilder.UseWebApi( config ); } }
نظرات مطالب
EF Code First #11
{هم در برنامههای وب میتونه استفاده شود و هم ویندوزی}
خوب این هم باز یه نکته است. اون UoW که گفتم بالا، دقیقا مربوط میشه به این بخش. مثال:
تو کامنت قبلی از استقلال لایه سرویس عرض کردم. حالا اضافه میکنم، سرویس من داره از EF استفاده میکنه. اما مستقیما نمیاد DbContext رو صدا بزنه. بلکه اون رو از یه UoW میگیره. خاصیتش اینه که UoW من از طریق یه HttpModule وهله سازی و نابود میشه. تو نابود شدنش dirty بودنش رو بررسی و در صورت نیاز commit میشه.
حالا چطور برم رو ویندوز؟ HttpContext اگه null باشه پس رو وب نیستم! راهکارم شبیه سازی HttpContext مثلا با یه کلاس به اسم HatContext هست. حالا میتونم لایه سرویس رو هم در وب و هم در ویندوز استفاده کنم.
UoWم هم سه مرحله توسعه داره. بالاترین سطح، فقط IsDirty رو در اختیار میذاره. سطح دوم فقط Commit و Rollback و سطح سوم TContext. سطح دوم و سوم فقط در اختیار سرویس قرار داره (internal). سطح اول در اختیار کل برنامه است (public). پس IsDirty، مثلا تو WPF خیلی میتونه مفید واقع بشه. حالا Forward کردن انواع (مثلا با StructureMap) کمکم میکنه که دسترسی به هر کدوم از interface های سه گانه بالا، فقط یه نمونه رو برگردونه.
{این یه مثال انتزاعی نیست؛ دقیقا یه پروژه ی واقعی بود که درگیرش بودم.}
خوب این هم باز یه نکته است. اون UoW که گفتم بالا، دقیقا مربوط میشه به این بخش. مثال:
تو کامنت قبلی از استقلال لایه سرویس عرض کردم. حالا اضافه میکنم، سرویس من داره از EF استفاده میکنه. اما مستقیما نمیاد DbContext رو صدا بزنه. بلکه اون رو از یه UoW میگیره. خاصیتش اینه که UoW من از طریق یه HttpModule وهله سازی و نابود میشه. تو نابود شدنش dirty بودنش رو بررسی و در صورت نیاز commit میشه.
حالا چطور برم رو ویندوز؟ HttpContext اگه null باشه پس رو وب نیستم! راهکارم شبیه سازی HttpContext مثلا با یه کلاس به اسم HatContext هست. حالا میتونم لایه سرویس رو هم در وب و هم در ویندوز استفاده کنم.
UoWم هم سه مرحله توسعه داره. بالاترین سطح، فقط IsDirty رو در اختیار میذاره. سطح دوم فقط Commit و Rollback و سطح سوم TContext. سطح دوم و سوم فقط در اختیار سرویس قرار داره (internal). سطح اول در اختیار کل برنامه است (public). پس IsDirty، مثلا تو WPF خیلی میتونه مفید واقع بشه. حالا Forward کردن انواع (مثلا با StructureMap) کمکم میکنه که دسترسی به هر کدوم از interface های سه گانه بالا، فقط یه نمونه رو برگردونه.
{این یه مثال انتزاعی نیست؛ دقیقا یه پروژه ی واقعی بود که درگیرش بودم.}
یکی دیگر از تغییرات ASP.NET Core با نگارشهای قبلی آن، تغییرات اساسی در مورد نحوهی کار با تنظیمات برنامه و فایلهای مرتبط با آنها است. در ASP.NET Core میتوانید:
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آنها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شدهی از فایلهای کانفیگ به کلاسها پیش بینی شدهاست.
- امکان بارگذاری مجدد فایلهای کانفیگ درصورت تغییر، بدون ریاستارت کل برنامه وجود دارد.
- امکان تزریق وابستگیهای تنظیمات برنامه، به قسمتهای مختلف آن پیش بینی شدهاست.
نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON
برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بستهی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:
البته همانطور که در تصویر مشاهده میکنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بستههای مرتبط با خواندن فایلهای کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
افزودن یک فایل کانفیگ JSON دلخواه
بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
در نگارشهای پیشین ASP.NET که از web.config برای تعریف تنظیمات برنامه استفاده میشد، حالت پیش فرض ذکر تنظیمات برنامه میتوانست تنها یک سطحی و با ساختار ذیل باشد (البته امکان کدنویسی و نوشتن مداخل سفارشی هم وجود داشت؛ ولی حالت پیش فرض appSettings، تنها key/valueهای یک سطحی هستند):
اما اکنون یک فایل JSON را با هر تعداد سطح مورد نیاز میتوان تعریف و استفاده کرد و برای اینکار نیازی به نوشتن کدهای سفارشی تعریف مداخل خاص، وجود ندارد.
در فایل JSON فوق، نمونهای از key/valueها، آرایهها و اطلاعات چندین سطحی را مشاهده میکنید.
خواندن فایل تنظیمات appsettings.json در برنامه
پس از نصب پیشنیاز خواندن فایلهای کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازندهی جدیدی را به آن اضافه کنید:
در اینجا نحوهی خواندن فایل کانفیگ جدید appsettings.json را مشاهده میکنید. چند نکته در اینجا حائز اهمیت هستند:
الف) این خواندن، در سازندهی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آنرا به یک خاصیت عمومی انتساب دادهایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شدهاست. این اطلاعات را میتوان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه میکنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت میشود.
دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot
تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شدهی در این اینترفیس به چه صورتی است؟
همانطور که مشاهده میکنید، هر سطح از سطح قبلی آن با : جدا شدهاست. همچنین اعضای آرایه، دارای ایندکسهای 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات میتوان نوشت:
خاصیت Configuration نیز در نهایت بر اساس key/valueها کار میکند و این keyها اگر چند سطحی بودند، با : از هم جدا میشوند و اگر نیاز به دسترسی اعضای خاصی از آرایهها وجود داشت میتوان آن ایندکس خاص را در انتهای زنجیره ذکر کرد. همچنین در اینجا نحوهی استخراج تمام اعضای یک آرایه را نیز مشاهده میکنید.
یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن میتوان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
در متد GetValue، آرگومان جنریک آن، یک کلاس را نیز میپذیرد. یعنی میتوان خواص تو در توی مشخص شدهی با : را به یک کلاس نیز نگاشت کرد. در اینجا مقدار کلید معرفی شده، اولین سطحی خواهد بود که باید این اطلاعات از آن استخراج و نگاشت شوند.
سرویس IConfigurationRoot قابل تزریق است
در قسمت قبل، سرویسها و تزریق وابستگیها را بررسی کردیم. نکتهی جالبی را که میتوان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
به تمام قسمتهای برنامه است. برای نمونه در همان مثال قسمت قبل، قصد داریم تنظیمات برنامه را در لایه سرویس آن خوانده و مورد استفاده قرار دهیم. برای اینکار باید مراحل ذیل طی شوند:
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده میکنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آنرا چگونه باید از خاصیت مرتبط با آن دریافت کند:
و یا اگر از همان IoC Container توکار ASP.NET Core استفاده میکنید، روش انجام اینکار در متد ConfigureServices به صورت زیر است:
طول عمر آن هم singleton مشخص شدهاست تا تنها یکبار وهله سازی و سپس کش شود (مناسب برای کار با تنظیمات سراسری برنامه).
ب) فایل project.json کتابخانهی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
این وابستگی امکان دسترسی به اینترفیس IConfigurationRoot را در اسمبلیهای دیگر میسر میکند.
ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازندهی سرویس MessagesService تزریق میکنیم:
در ادامه، نحوهی استفادهی از آن، همانند نکاتی است که در قسمت «دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot» عنوان شد.
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده میکند:
چنین خروجی را خواهیم داشت:
خواندن تنظیمات از حافظه
الزاما نیازی به استفاده از فایلهای JSON و یا XML در اینجا وجود ندارد. ابتداییترین حالت کار با بستهی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا میتوان لیستی از key/valueها را ذکر کرد:
و نحوهی کار با آن نیز همانند قبل است:
امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی
در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder میتوان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایلهای کانفیگ دیگر را اضافه کرد:
و نکتهی مهم اینجا است که تنظیمات فایل دوم، تنظیمات مشابه فایل اول را بازنویسی میکند.
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی میکند.
امکان دسترسی به متغیرهای محیطی سیستم عامل
در انتهای زنجیرهی ConfigurationBuilder میتوان متد AddEnvironmentVariables را نیز ذکر کرد:
این متد سبب میشود تا تمام اطلاعات قسمت Environment سیستم عامل، به مجموعهی تنظیمات جاری اضافه شوند (در صورت نیاز) که نمونهای از آنرا در تصویر ذیل مشاهده میکنید:
امکان نگاشت تنظیمات برنامه به کلاسهای متناظر
کار کردن با key/valueهای رشتهای، هرچند روش پایهای استفادهی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شدهاست. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
مثال جاری که بر اساس ASP.NET Core Web Application و با قالب خالی آن ایجاد شدهاست، دارای نام فرضی Core1RtmEmptyTest است. در همین پروژه بر روی پوشهی src کلیک راست کرده و گزینهی Add new project را انتخاب کنید و سپس یک پروژهی جدید از نوع NET Core -> Class library. را به آن با نام Core1RtmEmptyTest.ViewModels اضافه کنید (تصویر ذیل).
در این کتابخانهی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
از این جهت این کلاس را در یک library جداگانه قرار دادهایم تا بتوان از آن در لایهی سرویس و همچنین خود برنامه استفاده کرد. اگر این کلاس را در برنامهی اصلی قرار میدادیم، امکان دسترسی به آن در لایهی سرویس میسر نمیشد.
سپس به پروژهی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آنرا بتوان در پروژهی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
اکنون با فرض وجود تنظیمات خواندن فایل appsettings.json در سازندهی کلاس آغازین برنامه، نیاز است بستهی نیوگت Microsoft.Extensions.Configuration.Binder را نصب کنید:
و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوهی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
در اینجا مشخص شدهاست که کار وهله سازی کلاس SmtpConfig بر اساس اطلاعات قسمت smtp فایل کانفیگ تامین میشود. متغیر Configuration ایی که در اینجا استفاده شدهاست همان خاصیت عمومی public IConfigurationRoot Configuration کلاس آغازین برنامه است.
سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژهی Core1RtmEmptyTest.ViewModels به لایهی سرویس برنامه اضافه میکنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بستهی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایهی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگیهای فایل project.json این لایه چنین شکلی را پیدا میکند:
پس از ذخیره سازی این کلاس و بازیابی خودکار وابستگیهای آن، اکنون برای دسترسی به این تنظیم باید از اینترفیس ویژهی IOptions استفاده کرد (به همین جهت بستهی جدید نیوگت Microsoft.Extensions.Options را نصب کردیم):
همانطور که ملاحظه میکنید <IOptions<SmtpConfig به سازندهی کلاس تزریق شدهاست و سپس از طریق خاصیت Value آن میتوان به تمام اطلاعات کلاس SmtpConfig به شکل strongly typed دسترسی یافت.
اکنون اگر برنامه را جرا کنید، این خروجی را میتوان مشاهده کرد (که در آن آدرس Server دریافت شدهی از فایل کانفیگ نیز مشخص است):
البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگیها در تمام قسمتهای برنامه کار میکند. برای مثال در کنترلرها هم میتوان <IOptions<SmtpConfig را به همین نحو تزریق کرد.
نحوهی واکنش به تغییرات فایلهای کانفیگ
در نگارشهای قبلی ASP.NET، هر تغییری در فایل web.config، سبب ریاستارت شدن کل برنامه میشد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران میشد.
در ASP.NET Core، برنامهی وب ما دیگر متکی به فایل web.config نبوده و همچنین میتوان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایلهای کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
این پارامتر در صورت true بودن، به صورت خودکار سبب بارگذاری مجدد اطلاعات فایل کانفیگ میشود (بدون ریاستارت کل برنامه).
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آنها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شدهی از فایلهای کانفیگ به کلاسها پیش بینی شدهاست.
- امکان بارگذاری مجدد فایلهای کانفیگ درصورت تغییر، بدون ریاستارت کل برنامه وجود دارد.
- امکان تزریق وابستگیهای تنظیمات برنامه، به قسمتهای مختلف آن پیش بینی شدهاست.
نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON
برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بستهی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:
البته همانطور که در تصویر مشاهده میکنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بستههای مرتبط با خواندن فایلهای کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.Extensions.Configuration.Json": "1.0.0" },
افزودن یک فایل کانفیگ JSON دلخواه
بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
{ "Key1": "Value1", "Auth": { "Users": [ "Test1", "Test2", "Test3" ] }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
<appSettings> <add key="Logging-IncludeScopes" value="false" /> <add key="Logging-Level-Default" value="verbose" /> <add key="Logging-Level-System" value="Information" /> <add key="Logging-Level-Microsoft" value="Information" /> </appSettings>
در فایل JSON فوق، نمونهای از key/valueها، آرایهها و اطلاعات چندین سطحی را مشاهده میکنید.
خواندن فایل تنظیمات appsettings.json در برنامه
پس از نصب پیشنیاز خواندن فایلهای کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازندهی جدیدی را به آن اضافه کنید:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json"); Configuration = builder.Build(); }
الف) این خواندن، در سازندهی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آنرا به یک خاصیت عمومی انتساب دادهایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شدهاست. این اطلاعات را میتوان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه میکنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت میشود.
دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot
تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شدهی در این اینترفیس به چه صورتی است؟
همانطور که مشاهده میکنید، هر سطح از سطح قبلی آن با : جدا شدهاست. همچنین اعضای آرایه، دارای ایندکسهای 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات میتوان نوشت:
var key1 = Configuration["Key1"]; var user1 = Configuration["Auth:Users:0"]; var authUsers = Configuration.GetSection("Auth:Users").GetChildren().Select(x => x.Value).ToArray(); var loggingIncludeScopes = Configuration["Logging:IncludeScopes"]; var loggingLoggingLogLevelDefault = Configuration["Logging:LogLevel:Default"];
یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن میتوان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
var val = Configuration.GetValue<int>("key-name", defaultValue: 10);
سرویس IConfigurationRoot قابل تزریق است
در قسمت قبل، سرویسها و تزریق وابستگیها را بررسی کردیم. نکتهی جالبی را که میتوان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
public class Startup { public IConfigurationRoot Configuration { set; get; }
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده میکنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آنرا چگونه باید از خاصیت مرتبط با آن دریافت کند:
var container = new Container(); container.Configure(config => { config.For<IConfigurationRoot>().Singleton().Use(() => Configuration);
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
ب) فایل project.json کتابخانهی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
{ "dependencies": { //same as before "Microsoft.Extensions.Configuration.Abstractions": "1.0.0" }
ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازندهی سرویس MessagesService تزریق میکنیم:
public interface IMessagesService { string GetSiteName(); } public class MessagesService : IMessagesService { private readonly IConfigurationRoot _configurationRoot; public MessagesService(IConfigurationRoot configurationRoot) { _configurationRoot = configurationRoot; } public string GetSiteName() { var key1 = _configurationRoot["Key1"]; return $"DNT {key1}"; } }
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده میکند:
public void Configure( IApplicationBuilder app, IHostingEnvironment env, IMessagesService messagesService) { app.Run(async context => { var siteName = messagesService.GetSiteName(); await context.Response.WriteAsync($"Hello {siteName}"); }); }
خواندن تنظیمات از حافظه
الزاما نیازی به استفاده از فایلهای JSON و یا XML در اینجا وجود ندارد. ابتداییترین حالت کار با بستهی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا میتوان لیستی از key/valueها را ذکر کرد:
var builder = new ConfigurationBuilder() .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("the-key", "the-value"), });
var theValue = Configuration["the-key"];
امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی
در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder میتوان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایلهای کانفیگ دیگر را اضافه کرد:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("the-key", "the-value"), }) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); }
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی میکند.
امکان دسترسی به متغیرهای محیطی سیستم عامل
در انتهای زنجیرهی ConfigurationBuilder میتوان متد AddEnvironmentVariables را نیز ذکر کرد:
var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables();
امکان نگاشت تنظیمات برنامه به کلاسهای متناظر
کار کردن با key/valueهای رشتهای، هرچند روش پایهای استفادهی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شدهاست. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
{ "Key1": "Value1", "Auth": { "Users": [ "Test1", "Test2", "Test3" ] }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "Smtp": { "Server": "0.0.0.1", "User": "user@company.com", "Pass": "123456789", "Port": "25" } }
در این کتابخانهی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
namespace Core1RtmEmptyTest.ViewModels { public class SmtpConfig { public string Server { get; set; } public string User { get; set; } public string Pass { get; set; } public int Port { get; set; } } }
سپس به پروژهی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آنرا بتوان در پروژهی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
{ "dependencies": { // same as before "Core1RtmEmptyTest.ViewModels": "1.0.0-*" },
و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوهی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
public IServiceProvider ConfigureServices(IServiceCollection services) { services.Configure<SmtpConfig>(options => Configuration.GetSection("Smtp").Bind(options));
سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژهی Core1RtmEmptyTest.ViewModels به لایهی سرویس برنامه اضافه میکنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بستهی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایهی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگیهای فایل project.json این لایه چنین شکلی را پیدا میکند:
"dependencies": { "Core1RtmEmptyTest.ViewModels": "1.0.0-*", "Microsoft.Extensions.Configuration.Abstractions": "1.0.0", "Microsoft.Extensions.Options": "1.0.0", "NETStandard.Library": "1.6.0" }
public interface IMessagesService { string GetSiteName(); } public class MessagesService : IMessagesService { private readonly IConfigurationRoot _configurationRoot; private readonly IOptions<SmtpConfig> _settings; public MessagesService(IConfigurationRoot configurationRoot, IOptions<SmtpConfig> settings) { _configurationRoot = configurationRoot; _settings = settings; } public string GetSiteName() { var key1 = _configurationRoot["Key1"]; var server = _settings.Value.Server; return $"DNT {key1} - {server}"; } }
اکنون اگر برنامه را جرا کنید، این خروجی را میتوان مشاهده کرد (که در آن آدرس Server دریافت شدهی از فایل کانفیگ نیز مشخص است):
البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگیها در تمام قسمتهای برنامه کار میکند. برای مثال در کنترلرها هم میتوان <IOptions<SmtpConfig را به همین نحو تزریق کرد.
نحوهی واکنش به تغییرات فایلهای کانفیگ
در نگارشهای قبلی ASP.NET، هر تغییری در فایل web.config، سبب ریاستارت شدن کل برنامه میشد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران میشد.
در ASP.NET Core، برنامهی وب ما دیگر متکی به فایل web.config نبوده و همچنین میتوان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایلهای کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)