اشتراکها
انتشار SqlCeToolbox 4.7.x
LightInject در حال حاضر یکی از قدرتمندترین IoC Containerها است که از لحاظ سرعت و کارآیی در بالاترین جایگاه در میان IoC Containerهای موجود قرار دارد. جهت بررسی کارایی IoC Containerها میتوانید به این لینک مراجعه کنید . LightInject یک IoC Container فوق العاده سبک وزن میباشد که تمامی قابلیتهای متداولی که از یک Service Container انتظار میرود را شامل میشود. تنها شامل یک فایل .cs میباشد که تمامی کدهای آن در همین یک فایل نوشته شدهاند. در پروژههای کوچک تا بزرگ بدون از دست دادن کارآیی، با بالاترین سرعت ممکن عمل تزریق وابستگی را انجام میدهد. در این مجموعه مقالات به بررسی کامل این IoC Container میپردازیم و تمامی قابلیتهای آن را آموزش میدهیم.
نحوه نصب و راه اندازی LightInject
در پنجره Package Manager Console میتوانید با نوشتن دستور ذیل، نسخه باینری آن را نصب کنید که به فایل .dll آن Reference میدهد.
PM> Install-Package LightInject
PM> Install-Package LightInject.Source
آماده سازی پروژه نمونه
قبل از شروع کار با LightInject، یک پروژه Windows Forms Application را با ساختار کلاسهای ذیل ایجاد نمایید. (در مقالات بعدی و پس از آموزش کامل LightInject نحوه استفاده از آن را در ASP.NET MVC نیز آموزش میدهیم)
public class PersonModel { public int Id { get; set; } public string Name { get; set; } public string Family { get; set; } public DateTime Birth { get; set; } } public interface IRepository<T> where T:class { void Insert(T entity); IEnumerable<T> FindAll(); } public interface IPersonRepository:IRepository<PersonModel> { } public class PersonRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public interface IPersonService { void Insert(PersonModel entity); IEnumerable<PersonModel> FindAll(); } public class PersonService:IPersonService { private readonly IPersonRepository _personRepository; public PersonService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { _personRepository.Insert(entity); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } }
PersonModel: ساختار داده ای جدول Person در سمت Application، که در لایه Domain Model ایجاد میگردد.
توجه: جهت سهولت تست و تسریع کدنویسی از لایه بندی و از کلاسهای ViewModel استفاده نکردیم.
IRepository: یک Interface عمومی برای تمامی Interfaceهای مربوط به Repository که عملیات مربوط به پایگاه داده مثل بروزرسانی و واکشی اطلاعات را انجام میدهند.
IPersonRepository: واسط بین لایه Service و لایه Repository میباشد.
PersonRepository: پیاده سازی واقعی عملیات مربوط به پایگاه داده برای PersonModel میباشد. به کلاسهایی که حاوی پیاده سازی واقعی کد میباشند Concrete Class میگویند.
IPersonService: واسط بین رابط کاربری و لایه سرویس میباشد. رابط کاربری به جای دسترسی مستقیم به PersonService از IPersonService استفاده میکند.
PersonService: دریافت درخواستهای رابط کاربری و بررسی قوانین تجاری، سپس ارسال درخواست به لایه Repository در صورت صحت درخواست، و در نهایت ارسال پاسخ دریافتی به رابط کاربری. در واقع واسطی بین Repository و UI میباشد.
پس از ایجاد ساختار فوق کد مربوط به Form1 را بصورت زیر تغییر دهید.
public partial class Form1 : Form { private readonly IPersonService _personService; public Form1(IPersonService personService) { _personService = personService; InitializeComponent(); } }
در کد فوق به منظور ارتباط با سرویس از IPersonService استفاده نمودیم که به عنوان پارامتر ورودی برای سازنده Form1 تعریف شده است. حتما با Dependency Inversion و انواع Dependency Injection آشنا هستید که به سراغ مطالعه این مقاله آمدید و علت این نوع کدنویسی را هم میدانید. بنابراین توضیح بیشتری در این مورد نمیدهم.
حال اگر برنامه را اجرا کنید در Program.cs با خطای عدم وجود سازنده بدون پارامتر برای Form1 مواجه میشوید که کد آن را باید به صورت زیر تغییر میدهیم.
static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); var container = new ServiceContainer(); container.Register<IPersonService, PersonService>(); container.Register<IPersonRepository, PersonRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>())); }
کلاس ServiceContainer وظیفهی Register کردن یک کلاس را برای یک Interface دارد. زمانی که میخواهیم Form1 را نمونه سازی نماییم و Application را راه اندازی کنیم، باید نمونه ای را از جنس IPersonService ایجاد نموده و به سازندهی Form1 ارسال نماییم. با رعایت اصل DIP، نمونه سازی واقعی یک کلاس لایه دیگر، نباید در داخل کلاسهای لایه جاری انجام شود. برای این منظور از شیء container استفاده نمودیم و توسط متد GetInstance، نمونهای از جنس IPersonService را ایجاد نموده و به Form1 پاس دادیم. حال container از کجا متوجه میشود که چه کلاسی را برای IPersonService نمونه سازی نماید؟
در خطوط قبلی توسط متد Register، کلاس PersonService را برای IPersonService ثبت نمودیم. container نیز برای نمونه سازی به کلاس هایی که برایش Register نمودیم مراجعه مینماید و نمونه سازی را انجام میدهد. جهت استفاده از PersonService به پارامتر ورودی IPersonRepository برای سازندهی آن نیاز داریم که کلاس PersonRepository را برای IPersonRepository ثبت کردیم.
حال اگر برنامه را اجرا کنید، به درستی اجرا خواهد شد. برنامه را متوقف کنید و به کد موجود در Program.cs مراجعه نموده و دو خط مربوط به Register را Comment نمایید. سپس برنامه را اجرا کنید و خطای تولید شده را ببینید. این خطا بیان میکند که امکان نمونه سازی برای IPersonService را ندارد. چون قبلا هیچ کلاسی را برای آن Register نکرده ایم.
Named Services
در برخی مواقع، بیش از یک کلاس وجود دارند که ممکن است از یک Interface ارث بری نمایند. در این حالت و در زمان Register، باید به ServiceContainer بگوییم که کدام کلاس را باید نمونه سازی نماید. برای بررسی این موضوع، کلاسهای زیر را به ساختار پروژه اضافه نمایید.
public class WorkerModel:PersonModel { public ManagerModel Manager { get; set; } } public class ManagerModel:PersonModel { public IEnumerable<WorkerModel> Workers { get; set; } } public class WorkerRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public class ManagerRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public class WorkerService:IPersonService { private readonly IPersonRepository _personRepository; public WorkerService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { var worker = entity as WorkerModel; _personRepository.Insert(worker); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } } public class ManagerService:IPersonService { private readonly IPersonRepository _personRepository; public ManagerService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { var manager = entity as ManagerModel; _personRepository.Insert(manager); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } }
دو کلاس Manager و Worker به همراه سرویسها و Repository هایشان اضافه شده اند که از IPersonService و IPersonRepository مشتق شده اند.
حال کد کلاس Program را به صورت زیر تغییر میدهیم
... var container = new ServiceContainer(); container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>(); container.Register<IPersonRepository, PersonRepository>(); container.Register<IPersonRepository, WorkerRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>()));
در کد فوق، چون WorkerService بعد از PersonService ثبت یا Register شده است، LightInject در زمان ارسال پارامتر به Form1، نمونه ای از کلاس WorkerService را ایجاد میکند. اما اگر بخواهیم از کلاس PersonService نمونه سازی نماید باید کد را به صورت زیر تغییر دهیم.
... container.Register<IPersonService, PersonService>("PersonService"); container.Register<IPersonService, WorkerService>(); container.Register<IPersonRepository, PersonRepository>(); container.Register<IPersonRepository, WorkerRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>("PersonService")));
اگر در زمان ثبت، نامی را به نمونهی مورد نظر اختصاص داده باشیم، و فقط یک Register برای آن Interface معرفی نموده باشیم، در زمان نمونه سازی، LightInject آن نمونه را به عنوان سرویس پیش فرض در نظر میگیرد.
container.Register<IPersonService, PersonService>("PersonService"); Application.Run(new Form1(container.GetInstance<IPersonService>()));
IEnumerable<T>
زمانی که چند کلاس را که از یک Interface مشتق شده اند، با هم Register مینمایید، LightInject این قابلیت را دارد که این کلاسهای Register شده را در قالب یک لیست شمارشی برگردانید.
container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>("WorkerService"); var personList = container.GetInstance<IEnumerable<IPersonService>>();
container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>("WorkerService"); var personList = container.GetAllInstances<IPersonService>();
LightInject از Collectionهای زیر نیز پشتیبانی مینماید:
- Array
- ICollection<T>
- IList<T>
- IReadOnlyCollection<T>
- IReadOnlyList<T>
Values
توسط LightInject میتوانید مقادیر ثابت را نیز تعریف کنید
container.RegisterInstance<string>("SomeValue"); var value = container.GetInstance<string>();
container.RegisterInstance<string>("SomeValue","String1"); container.RegisterInstance<string>("OtherValue","String2"); var value = container.GetInstance<string>("String2");
در این آموزش قصد دارم چگونگی ایجاد یک سیستم اعلام وضعیت آب و هوا را مشابه آنچه که در سایت گوگل میبینید برای شما توضیح دهم. باید توجه داشت من این آموزش را با ASP.NET MVC نوشتم ولی شما میتوانید با اندک تغییراتی در کدها، آنرا در ASP.NET وب فرمز نیز استفاده کنید. برای گرفتن آب و هوای هر شهر از Rssهای اعلام وضعیت آب و هوای یاهو استفاده میکنم و توضیح خواهم داد که چگونه با Rss آن کار کنید.
Rss آب و هوای هر شهر در یاهو به صورت یک لینک یکتا میباشد؛ به شکل زیر :
و این لینک http://weather.yahooapis.com/forecastrss?w=28350859&u=c اطلاعات آب و هوای تهران را در قالب یک RSS به شما نمایش خواهد داد.
خوب، حالا پارامتر دوم یعنی پارامتر u چکاری را انجام میدهد؟
* چنانچه مقدار پارامتر u برابر c باشد، یعنی شما دمای آب و هوای شهر مد نظر را بر اساس سانتیگراد میخواهید.
* اگر مقدار پارامتر u برابر f باشد، یعنی شما دمای آب و هوای آن شهر مورد نظر را بر اساس فارنهایت میخواهید.
برای گرفتن WOEID شهرها هم به این سایت بروید http://woeid.rosselliot.co.nz و اسم هر شهری که میخواهید بزنید تا WOEID را به شما نمایش دهد.
در این مثال من از یک DropDown استفاده کردم که کاربر با انتخاب هر شهر از DropDown، آب و هوای آن شهر را مشاهده میکند.
Action مربوط به صفحهی Index به صورت زیر میباشد :
در اینجا من لیست شهرها را از جدول میخوانم که البته این جدول را چون بخش مهمی نبود و فقط شامل ID و نام شهرها بود در فایل ضمیمه قرار ندادم و نام شهرها و ID آنها را بر عهدهی خودتان گذاشتم.
حال تابعی را که آب و هوای مربوط به هر شهر را نمایش میدهد، به شرح زیر است:
قسمت SwitchCase، مقدار و Value مربوط به هر آیتم DropDown را که شامل یک اسم شهر است، میگیرد و RSS مربوط به آن شهر را بر میگرداند.
حالا کد مربوط به خواندن فایل Rss را برایتان توضیح میدهم : حلقهی for 0 تا 4 (که در کد بالا مشاهده میکنید)یعنی اطلاعات 4 روز آینده را برایم برگردان.
من تگهای Code ، Day ، Low ، High و text فایل RSS را در این حلقه For میخوانم که البته مقادیر این 4 روز را در لیستی اضافه میکنم که نوع این لیست هم از نوع YahooWeatherRssItem میباشد. من این کلاس را در فایل ضمیمه قرار دادم. اکنون هر کدام از این تگها را برایتان توضیح میدهم:
code : هر آب و هوا کدی دارد .مثلا آب و هوای نیمه ابری یک کد ، آب و هوای آفتابی کدی دیگر و ...
Low: حداقل دمای آن روز را به ما میدهد .
High: حداکثر دمای آن روز را به میدهد .
day: نام روز از هفته را بر میگرداند مثلا شنبه ، یکشنبه و ....
text: که توضیحاتی میدهد مثلا اگر هوا آفتابی باشد مقدار sunny را بر میگرداند و ...
خوب، تا اینجا ما Rss مربوط به هر شهر را خواندیم حالا در قسمت Design باید چکار کنیم .
کدهای html صفحهی Index ما شامل کدهای زیر است :
و کدهای _Weather که Partial است به صورت زیر است:
من عکسهای مربوط به وضعیت آب و هوا را در فایل ضمیمه قرار دادم.
چنانچه در مورد RSS وضعیت آب و هوای یاهو اطلاعات دقیقتری را میخواهید بدانید به این لینک بروید.
در آموزش بعدی قصد دارم برایتان این بخش را توضیح دهم که بر اساس IP بازدید کننده سایت شما، اطلاعات آب و هوایی شهر بازدید کننده را برایش در سایت نمایش دهد.
Files-06bf65bac63d4dd694b15fc24d4cb074.zip
موفق باشید
Rss آب و هوای هر شهر در یاهو به صورت یک لینک یکتا میباشد؛ به شکل زیر :
http://weather.yahooapis.com/forecastrss?w=WOEID&u=c
حال میخواهم کوئری استرینگهای این لینک را برای شما توضیح دهم. هر شهری بر روی کرهی زمین یک WOEID یکتا و منحصر بفرد دارد که شما به پارامتر w عدد WOEID شهر موردنظر خود را میدهید. بعد از مقداردهی پارامتر w، وقتی این لینک را در آدرس بار مرورگر خود میزنید، RSS مربوط به آب و هوای آن شهر را به شما میدهد. مثلا WOEID تهران عدد 28350859 میباشد.و این لینک http://weather.yahooapis.com/forecastrss?w=28350859&u=c اطلاعات آب و هوای تهران را در قالب یک RSS به شما نمایش خواهد داد.
خوب، حالا پارامتر دوم یعنی پارامتر u چکاری را انجام میدهد؟
* چنانچه مقدار پارامتر u برابر c باشد، یعنی شما دمای آب و هوای شهر مد نظر را بر اساس سانتیگراد میخواهید.
* اگر مقدار پارامتر u برابر f باشد، یعنی شما دمای آب و هوای آن شهر مورد نظر را بر اساس فارنهایت میخواهید.
برای گرفتن WOEID شهرها هم به این سایت بروید http://woeid.rosselliot.co.nz و اسم هر شهری که میخواهید بزنید تا WOEID را به شما نمایش دهد.
در این مثال من از یک DropDown استفاده کردم که کاربر با انتخاب هر شهر از DropDown، آب و هوای آن شهر را مشاهده میکند.
Action مربوط به صفحهی Index به صورت زیر میباشد :
[HttpGet] public ActionResult Index() { ViewBag.ProvinceList = _RPosition.Positions; ShowWeatherProvince(8); return View(); }
حال تابعی را که آب و هوای مربوط به هر شهر را نمایش میدهد، به شرح زیر است:
public ActionResult ShowWeatherProvince(int dpProvince) { XDocument rssXml=null; CountryName CountryName = new CountryName(); if (dpProvince != 0) { switch (dpProvince) { case 1: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345768&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Sharqhi" }; break; } case 2: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345767&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Qarbi" }; break; } case 3: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254335&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Ardebil" }; break; } case 4: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Alborz" }; break; } case 5: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345787&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Esfahan" }; break; } case 6: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345775&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Ilam" }; break; } case 7: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254463&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Bushehr" }; break; } case 8: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Tehran" }; break; } case 9: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345769&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Chahar Mahall va Bakhtiari" }; break; } case 10: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=56189824&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Razavi Khorasan" }; break; } case 11: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Shomali Khorasan" }; break; } case 12: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Jonubi Khorasan" }; break; } case 13: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345778&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Khuzestan" }; break; } case 14: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255311&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Zanjan" }; break; } case 15: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345784&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Semnan" }; break; } case 16: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345770&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Sistan va Baluchestan" }; break; } case 17: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345772&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Fars" }; break; } case 18: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070200&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Qazvin" }; break; } case 19: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255062&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Qom" }; break; } case 20: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345779&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kordestan" }; break; } case 21: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254796&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kerman" }; break; } case 22: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254797&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kermanshah" }; break; } case 23: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345771&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Kohgiluyeh va Buyer Ahmad" }; break; } case 24: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070201&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Golestan" }; break; } case 25: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345773&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Gilan" }; break; } case 26: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345782&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Lorestan" }; break; } case 27: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345783&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Markazi" }; break; } case 28: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345780&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Mazandaran" }; break; } case 29: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254664&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Hamedan" }; break; } case 30: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345776&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Hormozgan" }; break; } case 31: { rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2253355&u=c"); CountryName = new CountryName() { Country = "Iran", City = "Yazd" }; break; } } ViewBag.Location = CountryName; XNamespace yWeatherNS = "http://xml.weather.yahoo.com/ns/rss/1.0"; List<YahooWeatherRssItem> WeatherList = new List<YahooWeatherRssItem>(); for (int i = 0; i < 4; i++) { YahooWeatherRssItem YahooWeatherRssItem = new YahooWeatherRssItem() { Code = Convert.ToInt32(rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("code").Value), Day = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("day").Value, Low = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("low").Value, High = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("high").Value, Text = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("text").Value, }; WeatherList.Add(YahooWeatherRssItem); } ViewBag.FeedList = WeatherList; } return PartialView("_Weather"); }
حالا کد مربوط به خواندن فایل Rss را برایتان توضیح میدهم : حلقهی for 0 تا 4 (که در کد بالا مشاهده میکنید)یعنی اطلاعات 4 روز آینده را برایم برگردان.
من تگهای Code ، Day ، Low ، High و text فایل RSS را در این حلقه For میخوانم که البته مقادیر این 4 روز را در لیستی اضافه میکنم که نوع این لیست هم از نوع YahooWeatherRssItem میباشد. من این کلاس را در فایل ضمیمه قرار دادم. اکنون هر کدام از این تگها را برایتان توضیح میدهم:
code : هر آب و هوا کدی دارد .مثلا آب و هوای نیمه ابری یک کد ، آب و هوای آفتابی کدی دیگر و ...
Low: حداقل دمای آن روز را به ما میدهد .
High: حداکثر دمای آن روز را به میدهد .
day: نام روز از هفته را بر میگرداند مثلا شنبه ، یکشنبه و ....
text: که توضیحاتی میدهد مثلا اگر هوا آفتابی باشد مقدار sunny را بر میگرداند و ...
خوب، تا اینجا ما Rss مربوط به هر شهر را خواندیم حالا در قسمت Design باید چکار کنیم .
کدهای html صفحهی Index ما شامل کدهای زیر است :
@{ ViewBag.Title = "Weather"; } <link href="~/Content/User/Weather/Weather.css" rel="stylesheet" /> @section scripts{ <script src="@Url.Content("~/Scripts/jquery-1.6.2.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> <script type="text/javascript"> $("#dpProvince").change(function () { $(this).parents("form").submit(); }); </script> } <h2>Weather</h2> <div id="Progress"> <img src="~/Images/User/Other/ajax-loader.gif" /> </div> <div id="BoxContent"> @Html.Partial("_Weather")</div> @using (Ajax.BeginForm(actionName: "ShowWeatherProvince", ajaxOptions: new AjaxOptions { UpdateTargetId = "BoxContent", LoadingElementId = "Progress", InsertionMode = InsertionMode.Replace })) { <div style="padding-top:15px;"> <div style="float:left; width:133px; ">Select Your Province</div> <div style="float:left"> @Html.DropDownList("dpProvince", new SelectList(ViewBag.ProvinceList, "Id", "Name"),"Select Your Province", new { @class = "webUserDropDown", @style = "width:172px" })</div> </div> }
@{ List<Weather.YahooWeatherRssItem> Feeds = ViewBag.FeedList; } <div> @{ HtmlString StartTable = new HtmlString("<table class='WeatherTable' cellspacing='0' cellpadding='0'><tbody><tr>"); HtmlString EndTable = new HtmlString("</tr></tbody></table>"); HtmlString StartTD = new HtmlString("<td>"); HtmlString EndTD = new HtmlString("</td>"); } <div style="width: 300px;"> @{ @StartTable foreach (var item in Feeds) { @StartTD <div>@item.Day</div> <div> @{ string FileName = ""; switch (item.Code) { case 0: { FileName = "/Images/User/Weather/Tornado.png"; break; } case 1: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 2: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 3: { FileName = "/Images/User/Weather/storm2.gif"; break; } case 4: { FileName = "/Images/User/Weather/15.gif"; break; } case 5: { FileName = "/Images/User/Weather/29.gif"; break; } case 6: { FileName = "/Images/User/Weather/29.gif"; break; } case 7: { FileName = "/Images/User/Weather/29.gif"; break; } case 8: { FileName = "/Images/User/Weather/26.gif"; break; } case 9: { FileName = "/Images/User/Weather/drizzle.png"; break; } case 10: { FileName = "/Images/User/Weather/26.gif"; break; } case 11: { FileName = "/Images/User/Weather/18.gif"; break; } case 12: { FileName = "/Images/User/Weather/18.gif"; break; } case 13: { FileName = "/Images/User/Weather/19.gif"; break; } case 14: { FileName = "/Images/User/Weather/19.gif"; break; } case 15: { FileName = "/Images/User/Weather/19.gif"; break; } case 16: { FileName = "/Images/User/Weather/22.gif"; break; } case 17: { FileName = "/Images/User/Weather/Hail.png"; break; } case 18: { FileName = "/Images/User/Weather/25.gif"; break; } case 19: { FileName = "/Images/User/Weather/dust.png"; break; } case 20: { FileName = "/Images/User/Weather/fog_icon.png"; break; } case 21: { FileName = "/Images/User/Weather/hazy_icon.png"; break; } case 22: { FileName = "/Images/User/Weather/2017737395.png"; break; } case 23: { FileName = "/Images/User/Weather/32.gif"; break; } case 24: { FileName = "/Images/User/Weather/32.gif"; break; } case 25: { FileName = "/Images/User/Weather/31.gif"; break; } case 26: { FileName = "/Images/User/Weather/7.gif"; break; } case 27: { FileName = "/Images/User/Weather/38.gif"; break; } case 28: { FileName = "/Images/User/Weather/6.gif"; break; } case 29: { FileName = "/Images/User/Weather/35.gif"; break; } case 30: { FileName = "/Images/User/Weather/7.gif"; break; } case 31: { FileName = "/Images/User/Weather/33.gif"; break; } case 32: { FileName = "/Images/User/Weather/1.gif"; break; } case 33: { FileName = "/Images/User/Weather/34.gif"; break; } case 34: { FileName = "/Images/User/Weather/2.gif"; break; } case 35: { FileName = "/Images/User/Weather/freezing_rain.png"; break; } case 36: { FileName = "/Images/User/Weather/30.gif"; break; } case 37: { FileName = "/Images/User/Weather/15.gif"; break; } case 38: { FileName = "/Images/User/Weather/15.gif"; break; } case 39: { FileName = "/Images/User/Weather/15.gif"; break; } case 40: { FileName = "/Images/User/Weather/12.gif"; break; } case 41: { FileName = "/Images/User/Weather/22.gif"; break; } case 42: { FileName = "/Images/User/Weather/22.gif"; break; } case 43: { FileName = "/Images/User/Weather/22.gif"; break; } case 44: { FileName = "/Images/User/Weather/39.gif"; break; } case 45: { FileName = "/Images/User/Weather/thundershowers.png"; break; } case 46: { FileName = "/Images/User/Weather/19.gif"; break; } case 47: { FileName = "/Images/User/Weather/thundershowers.png"; break; } case 3200: { FileName = "/Images/User/Weather/1211810662.png"; break; } } } <img alt='@item.Text' title='@item.Text' src='@FileName'> </div> <div> <span>@item.High°</span> <span>@item.Low°</span> </div> @EndTD } } @EndTable </div> </div>
چنانچه در مورد RSS وضعیت آب و هوای یاهو اطلاعات دقیقتری را میخواهید بدانید به این لینک بروید.
در آموزش بعدی قصد دارم برایتان این بخش را توضیح دهم که بر اساس IP بازدید کننده سایت شما، اطلاعات آب و هوایی شهر بازدید کننده را برایش در سایت نمایش دهد.
Files-06bf65bac63d4dd694b15fc24d4cb074.zip
موفق باشید
اشتراکها
کتابخانه folderselect
Hierarchical select plugin for jQuery. Demo
یکی از مواردی که در هر پروژهای به چشم میخورد و وجود دارد، نمایش
دادههای ذخیره شدهی در بانک اطلاعاتی، به کاربر میباشد. احتمالا وب سایتهایی را دیدهاید که تمامی اطلاعات را در یک صفحه بدون هیچ صفحه بندی به
کاربر نشان میدهند که حس خوبی را به کاربر استفاده کننده منتقل نمیکند و
نتیجه منفی هم بر روی سئو خواهد گذاشت ( عدم داشتن Urlهای منحصر بفرد به
ازای هر صفحه).
بعضا دیده میشود که برنامه نویس یک Paging را به صورت Ajax ی پیاده سازی میکند که با تغییر صفحهها، اطلاعات را خوانده و به کاربر نمایش میدهد و هیچ اتفاقی در آدرس بار صفحه نمیافتد و خیلی خرسند است از کاری که انجام دادهاست. در چنین مواردی اگر کاربر استفاده کننده از برنامه بخواهد لینک صفحه دهم گرید و یا لیست اطلاعات را برای کسی بفرستد، باید چکار کند؟
توضیح بالا صرفا به این دلیل بیان شد تا به ضرورت داشتن Urlهای منحصر بفرد برای هر Page برسیم؛ هر چند بحث جاری درباره سئو نیست. ولی ترجیح دادم در کنار موضوع مقاله، توجهی هم داشته باشیم به این موضوع که رعایت آن حس بهتری را به کاربران برنامه میدهد.
کتابخانههای زیادی برای صفحه بندی اطلاعات وجود دارند و یا اینکه بعضی از برنامه نویسان و یا شرکتها ترجیح میدهند خود چرخ را مطابق میل خود از نو طراحی کنند. در ادامه قصد داریم به پیاده سازی کتابخانه PagedList که خیلی محبوب و پر طرفدار میباشد در ASP.NET MVC بپردازیم.
1- ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن مینماییم:
2- بعد از نصب این کتابخانه، متد الحاقی ToPagedList در اختیار ما قرار
داده میشود که بر روی IQueryable , IEnumerable در دسترس میباشد.
3- در کنترلر فقط کافیست متد ToPagedList را فراخوانی کرده و مقدار بازگشتی را به View ارسال نمود.
4 - و در نهایت در داخل View (ها) فقط کافیست برای نمایش صفحه بندی، دستور Html.PagedListPager را فراخوانی کنیم.
چهار مورد فوق تقریبا تمام کارهایی است که باید انجام شوند تا قادر باشیم از این کتابخانه برای صفحه بندی اطلاعات استفاده کنیم. در ادامه نحوه پیاده سازی آن را به همراه چند مثال بیان میکنیم.
ابتدا یک مدل فرضی را همانند زیر تهیه میکنیم:
و کلاسی را همانند زیر برای دریافت یک لیست از پستها مینویسیم:
ابتدا یک کنترلر را ایجاد نمایید. در اکشن متدی که قصد داریم لیستی از
اطلاعات را به کاربر نمایش دهیم، باید یک متغییر از نوع int برای شماره
صفحه در نظر گرفته شود:
و در view مربوطه داریم:
توسط متد postService.Getall، تمامی پستها از دیتابیس خوانده شده که جمعا 12 رکورد میباشند. فراخوانی ToPagedList به تعداد پارامتر دوم رکوردها را بر میگرداند و در متغییر result قرار میدهد و در پایان برای نمایش صفحه بندی، اقدام به فراخوانی متد الحاقی PagedListPager نمودهایم.
بله، درست حدس زدهاید! این روش دارای یک عیب میباشد و آن این است که ابتدا ما تمامی رکوردها را از دیتابیس فراخونی کرده و بعد از آن به تعداد 10 رکورد را از آن انتخاب نمودهایم. هر چند در مثال جاری تعداد رکوردها زیاد نمیباشد، ولی با مرور زمان و حجیم شدن دیتابیس، کوئری فوق امکان دارد به کندی اجرا شود. به همین دلیل نیاز به متدی داریم که با توجه به صفحه جاری، تعداد n رکورد را از دیتابیس خوانده و نمایش دهد. برای این منظور متدی همانند زیر را به کلاس postService اضافه مینماییم:
از totalCount برای نگه داری جمع کل رکوردها استفاده میکنیم و قصد نداریم تمامی اطلاعات را از دیتابیس واکشی نماییم.
در صفحه بندی به صورت دستی، تا حدودی اکشن Index تغییر خواهد کرد. در این روش داریم:
نکته: مقدار PagedIndex نمیتواند صفر باشد؛ چون شروع اعداد صفحه بندی از یک هست. به این خاطر، PageIndex با یک واحد، جمع شده است. در روش قبلی مقدار پیش فرض آن را 1 قرار دادیم. ولی در این روش ابتدا یک واحد از آن کم میکنیم؛ به این خاطر که در متد Skip شاهد اطلاعات دقیقی باشیم. محتوای view به همان روش قبلی میباشد و نیازی به تغییر آن نیست.
مقدار page به صورت کوئری استرینگ به انتهای url اضافه خواهد شد. جهت نظم بخشیدن به آن میبایست اقدام به افزودن route سفارشی نمایید که در حالت پیش فرض به صورت زیر میباشد:
در روشهایی که شرح آنها گذشت، از viewbag برای انتقال دادهها استفاده کردیم. میتوان view مورد نظر را strongly-typed معرفی نمود و دادهها را از طریق return view به سمت view بفرستید. در این حالت در view مربوطه داریم :
و تغییر حلقه for به صورت زیر :
سفارشی کردن Url: فرض کنید همراه با کلیک بر روی شمارهی صفحات، بخواهیم یک سری دیتای دیگر را هم به اکشن پاس دهیم؛ برای مثال tag=mvc. برای این منظور داریم:
استایل خروجی html حاصل از Html.PagedListPager بر اساس کتابخانه Bootstrap میباشد. در صورتیکه در پروژه خود از این کتابخانه استفاده نمیکنید، میتوانید فقط فایل PagedList.css را از Nuget دریافت نموده و به پروژهی خود اضافه نمایید.
یکی از overloadهای Html.PagedListPager پارامتری را تحت عنوان PagedListRenderOptions دارد که از آن میتوانید برای پیکربندی صفحه بندی استفاده نمایید. برای نمونه نمایش فقط 5 صفحه:
همچنین قادر خواهید بود یکسری تنظیمات دستی را بر روی شماره صفحات تولید شده انجام دهید؛ برای نمونه تغییر عناوین Next , Prev با عناوین فارسی:
بعضا دیده میشود که برنامه نویس یک Paging را به صورت Ajax ی پیاده سازی میکند که با تغییر صفحهها، اطلاعات را خوانده و به کاربر نمایش میدهد و هیچ اتفاقی در آدرس بار صفحه نمیافتد و خیلی خرسند است از کاری که انجام دادهاست. در چنین مواردی اگر کاربر استفاده کننده از برنامه بخواهد لینک صفحه دهم گرید و یا لیست اطلاعات را برای کسی بفرستد، باید چکار کند؟
توضیح بالا صرفا به این دلیل بیان شد تا به ضرورت داشتن Urlهای منحصر بفرد برای هر Page برسیم؛ هر چند بحث جاری درباره سئو نیست. ولی ترجیح دادم در کنار موضوع مقاله، توجهی هم داشته باشیم به این موضوع که رعایت آن حس بهتری را به کاربران برنامه میدهد.
کتابخانههای زیادی برای صفحه بندی اطلاعات وجود دارند و یا اینکه بعضی از برنامه نویسان و یا شرکتها ترجیح میدهند خود چرخ را مطابق میل خود از نو طراحی کنند. در ادامه قصد داریم به پیاده سازی کتابخانه PagedList که خیلی محبوب و پر طرفدار میباشد در ASP.NET MVC بپردازیم.
1- ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن مینماییم:
Install-Package PagedList.Mvc
3- در کنترلر فقط کافیست متد ToPagedList را فراخوانی کرده و مقدار بازگشتی را به View ارسال نمود.
4 - و در نهایت در داخل View (ها) فقط کافیست برای نمایش صفحه بندی، دستور Html.PagedListPager را فراخوانی کنیم.
چهار مورد فوق تقریبا تمام کارهایی است که باید انجام شوند تا قادر باشیم از این کتابخانه برای صفحه بندی اطلاعات استفاده کنیم. در ادامه نحوه پیاده سازی آن را به همراه چند مثال بیان میکنیم.
ابتدا یک مدل فرضی را همانند زیر تهیه میکنیم:
public class Post { public int Id { get; set; } public string Title { get; set; } public string Body { get; set; } }
public static class PostService { public static IEnumerable<Post> posts = new List<Post>() { new Post{Id=1,Title="Title 1",Body="Body 1"}, new Post{Id=2,Title="Title 2",Body="Body 2"}, new Post{Id=3,Title="Title 3",Body="Body 3"}, new Post{Id=4,Title="Title 4",Body="Body 4"}, new Post{Id=5,Title="Title 5",Body="Body 5"}, new Post{Id=6,Title="Title 6",Body="Body 6"}, new Post{Id=7,Title="Title 7",Body="Body 7"}, new Post{Id=8,Title="Title 8",Body="Body 8"}, new Post{Id=9,Title="Title 9",Body="Body 9"}, new Post{Id=10,Title="Title 10",Body="Body 10"}, new Post{Id=11,Title="Title 11",Body="Body 11"}, new Post{Id=12,Title="Title 12",Body="Body 12"}, }; public static IEnumerable<Post> GetAll() { return posts .OrderBy(row => row.Id); } }
public ActionResult Index(int? page) { var pageNumber = page ?? 1; var posts = PostService.GetAll(); var result = posts.ToPagedList(pageNumber, 10); ViewBag.posts = result; return View(); }
@using PagedList.Mvc; @using PagedList; <link href="/Content/PagedList.css" rel="stylesheet" type="text/css" /> <h2>List of posts</h2> <ul> @foreach (var post in ViewBag.posts) { <li>@post.Title</li> } </ul> @Html.PagedListPager((IPagedList)ViewBag.posts, page => Url.Action("Index", new { page }))
بله، درست حدس زدهاید! این روش دارای یک عیب میباشد و آن این است که ابتدا ما تمامی رکوردها را از دیتابیس فراخونی کرده و بعد از آن به تعداد 10 رکورد را از آن انتخاب نمودهایم. هر چند در مثال جاری تعداد رکوردها زیاد نمیباشد، ولی با مرور زمان و حجیم شدن دیتابیس، کوئری فوق امکان دارد به کندی اجرا شود. به همین دلیل نیاز به متدی داریم که با توجه به صفحه جاری، تعداد n رکورد را از دیتابیس خوانده و نمایش دهد. برای این منظور متدی همانند زیر را به کلاس postService اضافه مینماییم:
public static IEnumerable<Post> GetAll(int page, int recordsPerPage,out int totalCount) { totalCount = posts.Count(); return posts .OrderBy(row => row.Id).Skip(page * recordsPerPage).Take(recordsPerPage); // in real projects change like this .skip(()=>resultforSkip).Take(()=>recordsPerPage ) }
در صفحه بندی به صورت دستی، تا حدودی اکشن Index تغییر خواهد کرد. در این روش داریم:
public ActionResult Index(int? page) { var pageIndex = (page ?? 1) - 1; var pageSize = 10; int totalPostCount; var posts = PostService.GetAll(pageIndex, pageSize, out totalPostCount); var result = new StaticPagedList<Post>(posts, pageIndex + 1, pageSize, totalPostCount); ViewBag.posts = result; return View(); }
مقدار page به صورت کوئری استرینگ به انتهای url اضافه خواهد شد. جهت نظم بخشیدن به آن میبایست اقدام به افزودن route سفارشی نمایید که در حالت پیش فرض به صورت زیر میباشد:
http://localhost:53192/?page=2
routes.MapRoute( name: "paging", url: "{controller}/{action}/{page}", defaults: new { controller = "Home", action = "Index", page = UrlParameter.Optional } );
در روشهایی که شرح آنها گذشت، از viewbag برای انتقال دادهها استفاده کردیم. میتوان view مورد نظر را strongly-typed معرفی نمود و دادهها را از طریق return view به سمت view بفرستید. در این حالت در view مربوطه داریم :
@model PagedList.IPagedList <pagedListmvc.Models.Post>
@foreach (var post in Model) { <li>@post.Title</li> }
سفارشی کردن Url: فرض کنید همراه با کلیک بر روی شمارهی صفحات، بخواهیم یک سری دیتای دیگر را هم به اکشن پاس دهیم؛ برای مثال tag=mvc. برای این منظور داریم:
@Html.PagedListPager( myList, page => Url.Action("Index", new { page = page, tag= "mvc" }) )
استایل خروجی html حاصل از Html.PagedListPager بر اساس کتابخانه Bootstrap میباشد. در صورتیکه در پروژه خود از این کتابخانه استفاده نمیکنید، میتوانید فقط فایل PagedList.css را از Nuget دریافت نموده و به پروژهی خود اضافه نمایید.
یکی از overloadهای Html.PagedListPager پارامتری را تحت عنوان PagedListRenderOptions دارد که از آن میتوانید برای پیکربندی صفحه بندی استفاده نمایید. برای نمونه نمایش فقط 5 صفحه:
@Html.PagedListPager((IPagedList)ViewBag.posts, page => Url.Action("Index", new { page = page }), PagedListRenderOptions.OnlyShowFivePagesAtATime)
@Html.PagedListPager((IPagedList)ViewBag.posts, page => Url.Action("Index", new { page = page }), new PagedListRenderOptions { LinkToFirstPageFormat = "<< ابتدا", LinkToPreviousPageFormat = "< قبلی", LinkToNextPageFormat = "بعدی>", LinkToLastPageFormat = "آخرین >>" })
همانطور که در قسمت قبل اشاره شد، توابع نیز یکی از ویژگیهای اصلی PowerShell هستند. قبل از بررسی بیشتر توابع بهتر است ابتدا با مفهوم script block آشنا شویم. script blocks به مجموعهایی از دستورات گفته میشود که داخل یک بلاک قرار میگیرند. در واقع هر چیزی داخل {} یک script block محسوب میشود (البته به جز hash tables). به عنوان مثال در کد زیر از یک script block مخصوص، با نام فیلتر استفاده شده است که یک ورودی برای پارامتر FilterScript مربوط به دستور Where-Object میباشد. چیزی که این script block را متمایز میکند، خروجی آن است. به این معنا که خروجی آن باید یک مقدار بولین باشد:
script blocks را به صورت مستقیم درون command line هم میتوانیم استفاده کنیم. به محض تایپ کردن } و زدن کلید enter، امکان نوشتن اسکریپتهای چندخطی را درون ترمینال خواهیم داشت. در نهایت با بستن script block و زدن کلید enter، از بلاک خارج خواهیم شد:
با اینکار یک بلاک از کد را داخل متغیری با اسم block ذخیره کردهایم. برای فراخوانی این قطعه کد میتوانیم از یک عملگر مخصوص با نام invocation operator یا call operator استفاده کنیم:
یا حتی میتوانیم از Invoke-Command نیز برای اجرای بلاک استفاده کنیم. همچنین از عملگر & برای فراخوانی یک expression رشتهایی نیز میتوان استفاده کرد:
البته این نکته را در نظر داشته باشید که & قادر به پارز کردن (parse) یک expression نیست. به عنوان مثال اجرای کد زیر با خطا مواجه خواهد شد (برای حل این مشکل میتوانید بجای آن از Invoke-Expression استفاده کنید که امکان پارز کردن پارامترها را نیز دارد):
اما با کمک advanced functions میتوانیم چنین قابلیتی را داشته باشیم:
یکی دیگر از ویژگیهای advanced functions امکان استفاده فلگ Verbose حین فراخوانی دستورات میباشد. به عنوان مثال قطعه کد زیر را در نظر بگیرید:
کاری که تابع فوق انجام میدهد، دریافت دیتای پیشبینی وضعیت آبوهوای یک شهر است. در حالت عادی فراخوانی تابع فوق پیامهای Verbose را نمایش نمیدهد. از آنجائیکه تابع فوق یک advanced function است، میتوانیم فلگ Verbose را نیز وارد کنیم. با اینکار به صورت صریح گفتهایم که پیامهای از نوع Verbose را نیز نمایش دهد:
در ادامه یک مثال از نحوه هندل کردن ورودیهای یک تابع را بررسی خواهیم کرد. تابع زیر یک لیست از URLها را از کاربر دریافت کرده و یک health check توسط دستور Test-Connection انجام میدهد. در کد زیر پارامتر Websites را با تعدادی اتریبیوت مزین کردهایم. توسط اتریبیوت Parameter تعیین کردهایم که ورودی الزامی است و همچنین مقدار آن میتواند از pipeline نیز دریافت شود. در ادامه توسط ValidatePattern یک عبارت باقاعده را برای بررسی صحیح بودن URL دریافتی نوشتهایم. از آنجائیکه ورودی از نوع آرایهایی از string تعریف شده است، این تست برای هر آیتم از آرایه بررسی خواهد شد. برای پارامتر دوم یعنی Count نیز رنج مقداری را که کاربر وارد میکند، حداقل ۳ و حداکثر ۳ انتخاب کردهایم:
یکی دیگر از اعتبارسنجیهایی که میتوانیم برای پارامترهای یک تابع انتخاب کنیم، ValidateScript است. توسط این اتریبیوت میتوانیم یک منطق سفارشی برای اعتبارسنجی مقادیر پارامترها بنویسیم. به عنوان مثال تابع فوق را به گونهایی تغییر خواهیم داد که لیست وبسایتها را از طریق یک فایل JSON دریافت کند. میخواهیم قبل از دریافت فایل مطمئن شویم که فایل، به صورت فیزیکی روی دیسک وجود دارد، در غیراینصورت باید یک خطا را به کاربر نمایش دهیم:
تابع Ping-Website را جهت بررسی فیچر جدیدی که همراه با دستور ForEach-Object استفاده میشود، تغییر دادهایم تا به صورت Parallel عمل کند؛ این قابلیت از نسخه ۷ به بعد به PowerShell اضافه شده است. از آنجائیکه این قابلیت باعث میشود script block مربوط به ForEach-Object درون یک context دیگر با نام runspace اجرا شود. در نتیجه برای دسترسی به متغیرهای بیرون از script block نیاز خواهیم داشت از یک متغیر خودکار تحتعنوان using قبل از نام متغیر و بعد از علامت $ استفاده کنیم. همچنین آرایه مثال قبل را نیز به ArrayList تغییر دادهایم. زیرا در حالت قبلی امکان تغییر سایز یک آرایه با سایز ثابت را نخواهیم داشت. نکته دیگری که در مورد کد فوق میتوان به آن توجه کرد، نال کردن خروجی متد Add مربوط به آرایهی Results است. همانطور که در قسمت قبل توضیح دادیم، از این تکنیک برای suppress کردن خروجی استفاده میکنیم و چون در اینجا خروجی متد Add یک عدد میباشد، با تکنیک فوق، خروجی را دیگر درون کنسول مشاهده نخواهیم کرد. توسط اتریبیوت Alias نیز نامهای دیگری را که میتوان برای پارامتر Path حین فراخوانی تابع استفاده کرد، تعیین کردهایم. لیست کامل اتریبیوتهایی را که میتوان برای پارامترهای یک تابع تعیین کرد، میتوانید در مستندات PowerShell ببینید.
خروجی بلاک فوق Some variable value: 20 خواهد بود؛ زیرا قبل از فراخوانی doSomeWork مقدار متغیر عددی someVariable را به ۲۰ تغییر دادهایم. برای script blocks این امکان را داریم که دقیقاً در همان جایی که بلاک را تعریف میکنیم، یک snapshot تهیه کنیم. در اینحالت خروجی، مقدار Some variable value: 10 خواهد شد:
یکسری بلاکهای ویژه نیز درون توابع و script blockها میتوانیم بنویسیم که اصطلاحاً به name blocks معروف هستند:
دلیل آن نیز این است که به صورت صریح کدها را درون بلاک process ننوشته بودیم. همانطور که عنوان شد، در حالت پیشفرض، بدنه توابع درون بلاک end قرار خواهند گرفت و تنها یکبار اجرا خواهند شد. بنابراین:
برای اینکار میتوانیم با کمک dynamic param یک پارامتر را در زمان اجرا ایجاده کرده و مقادیری را که کاربر برای ستونها مجاز است وارد کند، براساس هدر فایل CSV تنظیم کنیم:
درون کنسول PowerShell هم یک IntelliSense برای مقادیر مجاز نمایش داده خواهد شد:
Get-Process | Where-Object { $_.Name -eq 'Dropbox' }
PS /Users/sirwanafifi/Desktop> $block = { >> $newVar = 10 >> Write-Host $newVar >> }
PS /Users/sirwanafifi/Desktop> & $block
PS /Users/sirwanafifi/Desktop> & "Get-Process"
PS /Users/sirwanafifi/Desktop> & "1 + 1" or PS /Users/sirwanafifi/Desktop> & "Get-Process -Name Slack"
توابع
در قسمت قبل با نحوه ایجاد توابع آشنا شدیم. به این نوع توابع، basic functions گفته میشود و سادهترین نوع توابع در PowerShell هستند. همچنین خیلی محدود نیز میباشند؛ یکسری ورودی/خروجی دارند. برای کنترل بیشتر روی نحوه فراخوانی توابع (به عنوان مثال دریافت ورودی از pipeline و…) باید از advanced functions یا توابع پیشرفته استفاده کنیم. در واقع به محض استفاده از اتریبیوتی با نام [()CmdletBinding] تابع ما تبدیل به یک advanced function خواهد شد. منظور از دریافت ورودی از pipeline این است که بتوانیم خروجی دستورات را به تابعمان pipe کنیم اینکار در basic function امکانپذیر نیست: Function Add-Something { Write-Host "$_ World" } "Hello" | Add-Something
Function Add-Something { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] [string]$Name ) Write-Host "$Name World" } "Hello" | Add-Something
$API_KEY = "...." Function Read-WeatherData { [CmdletBinding()] Param( [Parameter(ValueFromPipeline = $true)] [string]$CityName ) $Url = "https://api.openweathermap.org/data/2.5/forecast?q=$CityName&cnt=40&appid=$API_KEY&units=metric" Try { Write-Verbose "Reading weather data for $CityName" $Response = Invoke-RestMethod -Uri $Url $Response.list | ForEach-Object { Write-Verbose "Processing $($_.dt_txt)" [PSCustomObject]@{ City = $Response.city.name DateTime = [DateTime]::Parse($_.dt_txt) Temperature = $_.main.temp Humidity = $_.main.humidity Pressure = $_.main.pressure WindSpeed = $_.wind.speed WindDirection = $_.wind.deg Cloudiness = $_.clouds.all Weather = $_.weather.main WeatherDescription = $_.weather.description } } | Where-Object { $_.DateTime.Date -eq (Get-Date).Date } Write-Verbose "Done processing $CityName" } Catch { Write-Error $_.Exception.Message } }
Read-WeatherData -CityName "London" -Verbose
هر چند این مقدار را همانطور که در قسمتهای قبلی عنوان شد میتوانیم تغییر دهیم که دیگر مجبور نباشیم با فراخوانی هر تابع، این فلگ را نیز ارسال کنیم. بیشتر دستورات native نیز قابلیت نمایش پیامهای Verbose را با ارسال همین فلگ در اختیارمان قرار میدهند. بنابراین بهتر است برای امکان مشاهده جزئیات بیشتر حین فراخوانی توابعمان از Write-Verbose استفاده کنیم. در ادامه اجزای دیگر توابع را بررسی خواهیم کرد (بیشتر این اجزا درون یک script block نیز قابل استفاده هستند)
کنترل کامل بر روی ورودیهای توابع
بر روی ورودیهای یک تابع میتوانیم کنترل نسبتاً کاملی داشتیم باشیم. PowerShell یک مجموعه وسیع از قابلیتها را برای هندل کردن پارامترها و همچنین اعتبارسنجی ورودیها ارائه میدهد. به عنوان مثال میتوانیم یک پارامتر را mandatory کنیم یا اینکه امکان positional binding و غیره را تعیین کنیم. اتریبیوت Parameter در واقع یک وهله از System.Management.Automation.ParameterAttribute میباشد. میتوانید با نوشتن دستور زیر لیستی از خواصی را که میتوانید همراه با این اتریبیوت تعیین کنید، مشاهده کنید:
PS /> [Parameter]::new() ExperimentName : ExperimentAction : None Position : -2147483648 ParameterSetName : __AllParameterSets Mandatory : False ValueFromPipeline : False ValueFromPipelineByPropertyName : False ValueFromRemainingArguments : False HelpMessage : HelpMessageBaseName : HelpMessageResourceId : DontShow : False TypeId : System.Management.Automation.ParameterAttribute
Function Ping-Website { [CmdletBinding()] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidatePattern('^www\..*')] [string[]]$Websites, [ValidateRange(1, 3)] [int]$Count = 3 ) $Results = @() $Websites | ForEach-Object { $Website = $_ $Result = Test-Connection -ComputerName $Website -Count $Count -Quiet $ResultText = $Result ? 'Success' : 'Failed' $Results += @{ Website = $Website Result = $ResultText } Write-Verbose "The result of pinging $Website is $ResultText" } $Results | ForEach-Object { $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $Count }; } } }
Function Ping-Website { [CmdletBinding()] Param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ If (-Not ($_ | Test-Path) ) { Throw "File or folder does not exist" } If (-Not ($_ | Test-Path -PathType Leaf) ) { Throw "The Path argument must be a file. Folder paths are not allowed." } If ($_ -NotMatch "(\.json)$") { throw "The file specified in the path argument must be either of type json" } Return $true })] [Alias("src", "source", "file")] [System.IO.FileInfo]$Path, [int]$Count = 1 ) $Results = [System.Collections.ArrayList]@() $Urls = Get-Content -Path $Path | ConvertFrom-Json $Urls | ForEach-Object -Parallel { $Website = $_.url $Result = Test-Connection -ComputerName $Website -Count $using:Count -Quiet $ResultText = $Result ? 'Success' : 'Failed' $Item = @{ Website = $Website Result = $ResultText } $null = ($using:Results).Add($Item) } $Results | ForEach-Object -Parallel { $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $using:Count }; } } }
نکته: اگر تابع فوق را همراه با فلگ Verbose فراخوانی کنیم، لاگهای موردنظر را درون کنسول مشاهده نخواهیم کرد؛ زیرا همانطور که اشاره شد script block درون یک context جدا اجرا میشود و باید متغیرهای خودکار مربوط به Output را مجدداً مقداردهی کنیم:
Function Ping-Website { [CmdletBinding()] Param( # As before ) # As before $Urls | ForEach-Object -Parallel { $DebugPreference = $using:DebugPreference $VerbosePreference = $using:VerbosePreference $InformationPreference = $using:InformationPreference # As before } # As before }
قابلیت تعریف بلاکها/توابع، به صورت تودرتو
درون توابع و script block امکان نوشتن بلاکهای تودرتو را نیز داریم:
$scriptBlock = { $logOutput = { param($message) Write-Host $message } [int]$someVariable = 10 $doSomeWork = { & $logOutput -message "Some variable value: $someVariable" } $someVariable = 20 & $doSomeWork }
$scriptBlock = { $logOutput = { param($message) Write-Host $message } [int]$someVariable = 10 $doSomeWork = { & $logOutput -message "Some variable value: $someVariable" }.GetNewClosure() $someVariable = 20 & $doSomeWork }
begin process end dynamicparam
درون یک تابع اگر هیچکدام از بلاکهای فوق استفاده نشود، به صورت پیشفرض بدنه تابع، درون بلاک end قرار خواهد گرفت. بلاک begin قبل از شروع pipeline اجرا میشود. process به ازای هر آیتم pipe شده اجرا خواهد شد. end نیز در پایان اجرا میشود. به عنوان مثال تابع زیر را در نظر بگیرید:
function Show-Pipeline { begin { Write-Host "Pipeline start" } process { Write-Host "Pipeline process $_" } end { Write-Host "Pipeline end $_" } }
در ادامه یکسری آیتم را به ورودی این تابع pipe خواهیم کرد:
PS /> 1..2 | Show-Pipeline Pipeline start Pipeline process 1 Pipeline process 2 Pipeline end 2
همانطور که مشاهده میکنید، به ازای هر آیتم pipe شده، یکبار بلاک process اجرا شده است. همچنین برای دسترسی به مقدار آیتم pipe شده نیز از متغیر خودکار _$ استفاده کردهایم (PSItem$ نیز به همین متغیر اشاره دارد).
با توجه به توضیحات named blockهای فوق، اکنون اگر بخواهیم نسخه اول تابع Ping-Website را با pipe کردن یک آرایه فراخوانی کنیم، خروجی که در کنسول نمایش داده خواهد شد، تنها آیتم آخر از آرایه خواهد بود:
PS /> "www.google.com", "www.yahoo.com" | Ping-Website Website Result Number Of Attempts ------- ------ ------------------ www.yahoo.com Success 3
Function Ping-Website { [CmdletBinding()] Param( # As before ) process { # As before } }
اینبار اگر تابع را مجدداً فراخوانی کنیم، خروجی مطلوب را نمایش خواهد داد:
PS /> "www.google.com", "www.yahoo.com" | Ping-Website Website Result Number Of Attempts ------- ------ ------------------ www.google.com Success 3 www.yahoo.com Success 3
بلاک dynamicparam
از این بلاک برای تعریف پارامترهای داینامیک که به صورت on the fly نیاز هست ایجاد شوند، استفاده میشود. برای درک بهتر آن فرض کنید میخواهیم تابعی را بنویسیم که امکان خواندن یک فایل CSV را به ما میدهد. تا اینجای کار توسط Import-CSV به یک خط دستور قابل انجام است. اما فرض کنید میخواهیم به کاربر این امکان را بدهیم که یک ستون موردنظر از فایل را مشاهده کند. همچنین میخواهیم یک اعتبارسنجی هم روی نام ستونی که کاربر قرار است وارد کند نیز داشته باشیم. به عنوان مثال یک فایل CSV با ستونهای name, lname, age داریم و کاربر میخواهد تنها ستون اول یک name را واکشی کند:
PS /> Read-Csv ./users.csv -Columns name
using namespace System.Management.Automation Function Read-Csv { Param ( [Parameter(Mandatory = $true, Position = 0)] [string]$Path ) DynamicParam { $firstLine = Get-Content $Path | Select-Object -First 1 [String[]]$headers = $firstLine -split ', ' $parameters = [RuntimeDefinedParameterDictionary]::new() $parameter = [RuntimeDefinedParameter]::new( 'Columns', [String[]], [Attribute[]]@( [Parameter]@{ Mandatory = $false; Position = 1 } [ValidateSet]::new($headers) ) ) $parameters.Add($parameter.Name, $parameter) Return $parameters } Begin { $csvContent = Import-Csv $Path If ($PSBoundParameters.ContainsKey('Columns')) { $columns = $PSBoundParameters['Columns'] $csvContent | Select-Object -Property $columns } Else { $csvContent } } }
نظرات مطالب
EF Code First #15
روش استفاده از MariaDb به همراه EF Code First در Blazor Server
سپس یک رشته اتصال مانند زیر در فایل appsettings.json ایجاد مینماییم:
حال در تنظیمات سرویسهای برنامه در Program.cs به شکل زیر عمل میکنیم:
برای این منظور میتوان از پروایدر Pomelo.EntityFrameworkCore.MySql استفاده نمود (لایسنس Open source MIT ). ابتدا توسط دستور زیر پکیج آن را نصب مینماییم:
Install-Package Pomelo.EntityFrameworkCore.MySql
"ConnectionStrings": { "MariaDbConnectionString": "server=localhost;user id=root;password=root;database=aspnetcore.mariadb" }
public void ConfigureServices(IServiceCollection services) { services.AddDbContextPool<ApplicationDbContext>(options => options .UseMySql( Configuration.GetConnectionString("MariaDbConnectionString"),MariaDbServerVersion.AutoDetect(Configuration.GetConnectionString("MariaDbConnectionString")) ) ); }
به راحتی بانک را در سرور ایجاد نموده و جداول را نیز میسازد. البته بنابر تنظیمات خاص یک سرور لینوکس ممکن است نیاز باشد تا ابتدا بانک به صورت دستی در سرور ایجاد شود و مابقی قضایا (ساخت جداول و ...) میتواند به پروایدر واگذار شود.
جهت اطلاعات بیشتر میتوانید به * و * مراجعه نمایید.در زمان ارائهی ASP.NET Core 2.1، ویژگی جدیدی به نام [ApiController] ارائه شد که با استفاده از آن، یکسری اعمال توکار جهت سهولت کار با Web API توسط خود فریمورک انجام میشوند؛ برای مثال عدم نیاز به بررسی وضعیت ModelState و بررسی خودکار آن با علامتگذاری یک کنترلر به صورت ApiController. یکی دیگر از این ویژگیهای توکار، تبدیل خروجی تمام status codeهای بزرگتر و یا مساوی 400 یا همان Bad Request، به شیء جدید و استاندارد ProblemDetails است:
بازگشت یک چنین خروجی یکدست و استانداردی، استفادهی از آنرا توسط کلاینتها، ساده و قابل پیشبینی میکند. البته باید درنظر داشت که اگر در اینحالت، برنامه یک استثنای معمولی را سبب شود، ProblemDetails ای بازگشت داده نمیشود. اگر برنامه در حالت توسعه اجرا شود، با استفاده از میانافزار app.UseDeveloperExceptionPage، یک صفحهی نمایش جزئیات خطا ظاهر میشود و اگر برنامه در حالت تولید و ارائهی نهایی اجرا شود، یک صفحهی خالی (بدون داشتن response body) با status code مساوی 500 بازگشت داده میشود. این کمبود ویژه و امکانات سفارشی سازی بیشتر آن، به صورت توکار به ASP.NET Core 7x اضافه شدهاند و دیگر نیازی به استفاده از کتابخانههای ثالث دیگری برای انجام آن نیست.
ProblemDetails بر اساس RFC7807 طراحی شدهاست
RFC7807، قالب استانداردی را برای ارائهی خطاهای HTTP APIها تعریف میکند تا نیازی به وجود تعاریف متعددی در این زمینه نباشد و خروجی آن قابل پیشبینی و قابل بررسی توسط تمام کلاینتهای یک API باشد. کلاس ProblemDetails در ASP.NET Core نیز بر همین اساس طراحی شدهاست.
این RFC دو فرمت خروجی را بر اساس مقدار مشخص شدهی در هدر Content-Type بازگشت داده شده، مجاز میداند:
که با توجه به این هدر ارسالی، اگر از یک کلاینت از نوع HttpClient استفاده کنیم، میتوان بر اساس مقدار ویژهی «application/problem+json» تشخیص داد که خروجی API دریافتی، به همراه خطا است و نحوهی پردازش آن به صورت زیر خواهد بود:
در اینجا بدنهی اصلی شیء ProblemDetails بازگشت داده شده، میتواند به همراه اعضای زیر باشد:
- type: یک رشتهاست که به آدرس مستندات HTML ای مرتبط با خطای بازگشت داده شده، اشاره میکند.
- title: رشتهای است که خلاصهی خطای رخداده را بیان میکند.
- detail: رشتهای است که توضیحات بیشتری را در مورد خطای رخداده، بیان میکند.
- instance: رشتهای است که به آدرس محل بروز خطا اشاره میکند.
- status: عددی است که بیانگر HTTP status code بازگشتی از سمت سرور است.
البته اگر ویژگی ApiController بر روی کنترلرهای خود استفاده نمیکنید، میتوانید این خروجی را به صورت زیر هم با استفاده از return Problem، تولید کنید:
امکان افزودن اعضای سفارشی به شیء ProblemDetails
امکان بسط این خروجی، با افزودن اعضای سفارشی نیز پیشبینی شدهاست. یک نمونهی متداول و پرکاربرد آن، بازگشت خطاهای مرتبط با اعتبارسنجی اطلاعات رسیدهاست:
در اینجا عضو جدید errors را بنابر نیاز این مسالهی خاص، مشاهده میکنید که در صورت استفاده از ویژگی ApiController بر روی کنترلرهای Web API، به صورت خودکار توسط ASP.NET Core تولید میشود و نیازی به تنظیم خاصی و یا کدنویسی اضافهتری ندارد. کلاس مخصوص آن نیز ValidationProblemDetails است.
جهت افزودن اعضای سفارشی دیگری به شیء ProblemDetails میتوان به صورت زیر عمل کرد:
شیء ProblemDetails، به همراه خاصیت Extensions است که میتوان به آن یک <Dictionary<string, object را انتساب داد و نمونهای از آنرا در مثال فوق مشاهده میکنید. این مثال سبب میشود تا عضو جدیدی با کلید دلخواه invalidParams، به همراه لیستی از name و reasonها به خروجی نهایی اضافه شود. مقدار این کلید، از نوع object است؛ یعنی هر شیء دلخواهی را در اینجا میتوان تعریف و استفاده کرد.
معرفی سرویس جدید ProblemDetails در دات نت 7
در دات نت 7 میتوان سرویسهای جدید ProblemDetails را به نحو زیر به برنامه اضافه کرد:
پس از آن به 3 روش مختلف میتوان از امکانات این سرویسها استفاده کرد:
الف) با اضافه کردن میانافزار مدیریت خطاها
پس از آن، هر استثنای مدیریت نشدهای نیز به صورت یک ProblemDetails ظاهر میشود و دیگر همانند قبل، سبب نمایش یک صفحهی خالی نخواهد شد.
ب) با افزودن میانافزار StatusCodePages
در این حالت مواردی که استثناء شمرده نمیشوند مانند 404، در صورت بروز رسیدن به یک مسیریابی یافت نشده و یا 405، در صورت درخواست یک HTTP method غیرمعتبر نیز توسط یک ProblemDetails استاندارد مدیریت میشوند.
ج) با افزودن میانافزار صفحهی استثناءهای توسعه دهندهها
به این ترتیب در خروجی ProblemDetails، اطلاعات بیشتری از استثناء رخداده، مانند استکتریس آن ظاهر خواهد شد.
امکان بازگشت سادهتر یک ProblemDetails سفارشی در دات نت 7
برای سفارشی سازی خروجی ProblemDetails، علاوه بر راهحلی که پیشتر در این مطلب مطرح شد، میتوان در دات نت 7 از روش تکمیلی ذیل نیز استفاده کرد:
به این ترتیب در صورت لزوم میتوان یک عضو سفارشی سراسری را به تمام اشیاء ProblemDetails برنامه به صورت خودکار اضافه کرد و یا اگر میخواهیم این مورد را کمی اختصاصیتر کنیم، میتوان به صورت زیر عمل کرد:
الف) تعریف یک ErrorFeature سفارشی
در ASP.NET Core میتوان به شیء HttpContext.Features قابل تنظیم در هر اکشن متدی، اشیاء دلخواهی را مانند شیء سفارشی فوق، اضافه کرد و سپس در قسمت options.CustomizeProblemDetails تنظیماتی که ذکر شد، به دریافت و تنظیم آن، واکنش نشان داد.
ب) تنظیم مقدار ErrorFeature سفارشی در اکشن متدها
پس از تعریف شیءایی که قرار است به HttpContext.Features اضافه شود، اکنون روش تنظیم و مقدار دهی آنرا در یک اکشن متد، در مثال فوق مشاهده میکنید.
ج) واکنش نشان دادن به دریافت ErrorFeature سفارشی
پس از تنظیم HttpContext.Features در اکشن متدی، میتوان در options.CustomizeProblemDetails فوق، توسط متد ctx.HttpContext.Features.Get به آن شیء خاص تنظیم شده، در صورت وجود دسترسی یافت و سپس جزئیات بیشتری را از آن استخراج و مقادیر ctx.ProblemDetails جاری را که قرار است به کاربر بازگشت داده شوند، بازنویسی کرد و یا تغییر داد.
امکان تبدیل سادهتر اطلاعات استثناءهای سفارشی به یک ProblemDetails سفارشی در دات نت 7
بجای استفاده از تنظیمات services.AddProblemDetails جهت بازنویسی مقدار شیء ProblemDetails بازگشتی، میتوان جزئیات میانافزار app.UseExceptionHandler را نیز سفارشی سازی کرد و به بروز استثناءهای خاصی واکنش نشان داد. برای مثال فرض کنید یک استثنای سفارشی را به صورت زیر طراحی کردهاید:
و سپس در اکشن متدی، سبب بروز آن شدهاید:
اکنون میتوان در میانافزار مدیریت استثناءهای برنامه، نسبت به مدیریت این استثناء خاص، واکشن نشان داد و ProblemDetails متناظری را تولید و بازگشت داد:
در اینجا نحوهی کار با سرویس توکار IProblemDetailsService و سپس دسترسی به IExceptionHandlerFeature و استثنای صادر شده را مشاهده میکنید. پس از آن بر اساس نوع و اطلاعات این استثناء، میتوان یک ProblemDetails مخصوص را تولید و در خروجی ثبت کرد.
{ "type": "https://example.com/probs/out-of-credit", "title": "You do not have enough credit.", "detail": "Your current balance is 30, but that costs 50.", "instance": "/account/12345/msgs/abc", "status": 403, }
ProblemDetails بر اساس RFC7807 طراحی شدهاست
RFC7807، قالب استانداردی را برای ارائهی خطاهای HTTP APIها تعریف میکند تا نیازی به وجود تعاریف متعددی در این زمینه نباشد و خروجی آن قابل پیشبینی و قابل بررسی توسط تمام کلاینتهای یک API باشد. کلاس ProblemDetails در ASP.NET Core نیز بر همین اساس طراحی شدهاست.
این RFC دو فرمت خروجی را بر اساس مقدار مشخص شدهی در هدر Content-Type بازگشت داده شده، مجاز میداند:
- JSON: “application/problem+json” media type
- XML: “application/problem+xml” media type
که با توجه به این هدر ارسالی، اگر از یک کلاینت از نوع HttpClient استفاده کنیم، میتوان بر اساس مقدار ویژهی «application/problem+json» تشخیص داد که خروجی API دریافتی، به همراه خطا است و نحوهی پردازش آن به صورت زیر خواهد بود:
var mediaType = response.Content.Headers.ContentType?.MediaType; if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) { var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(null, ct) ?? new ProblemDetails(); // ... }
- type: یک رشتهاست که به آدرس مستندات HTML ای مرتبط با خطای بازگشت داده شده، اشاره میکند.
- title: رشتهای است که خلاصهی خطای رخداده را بیان میکند.
- detail: رشتهای است که توضیحات بیشتری را در مورد خطای رخداده، بیان میکند.
- instance: رشتهای است که به آدرس محل بروز خطا اشاره میکند.
- status: عددی است که بیانگر HTTP status code بازگشتی از سمت سرور است.
البته اگر ویژگی ApiController بر روی کنترلرهای خود استفاده نمیکنید، میتوانید این خروجی را به صورت زیر هم با استفاده از return Problem، تولید کنید:
[HttpPost("/sales/products/{sku}/availableForSale")] public async Task<IActionResult> AvailableForSale([FromRoute] string sku) { return Problem( "Product is already Available For Sale.", "/sales/products/1/availableForSale", 400, "Cannot set product as available.", "http://example.com/problems/already-available"); }
امکان بسط این خروجی، با افزودن اعضای سفارشی نیز پیشبینی شدهاست. یک نمونهی متداول و پرکاربرد آن، بازگشت خطاهای مرتبط با اعتبارسنجی اطلاعات رسیدهاست:
HTTP/1.1 400 Bad Request Content-Type: application/problem+json Content-Language: en { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "User": [ "The user name is not verified." ] } }
جهت افزودن اعضای سفارشی دیگری به شیء ProblemDetails میتوان به صورت زیر عمل کرد:
namespace WebApplication.Controllers { [ApiController] [Route("[controller]")] public class DemoController : ControllerBase { [HttpPost] public ActionResult Post() { var problemDetails = new ProblemDetails { Detail = "The request parameters failed to validate.", Instance = null, Status = 400, Title = "Validation Error", Type = "https://example.net/validation-error", }; problemDetails.Extensions.Add("invalidParams", new List<ValidationProblemDetailsParam>() { new("name", "Cannot be blank."), new("age", "Must be great or equals to 18.") }); return new ObjectResult(problemDetails) { StatusCode = 400 }; } } public class ValidationProblemDetailsParam { public ValidationProblemDetailsParam(string name, string reason) { Name = name; Reason = reason; } public string Name { get; set; } public string Reason { get; set; } } }
معرفی سرویس جدید ProblemDetails در دات نت 7
در دات نت 7 میتوان سرویسهای جدید ProblemDetails را به نحو زیر به برنامه اضافه کرد:
services.AddProblemDetails();
الف) با اضافه کردن میانافزار مدیریت خطاها
app.UseExceptionHandler();
ب) با افزودن میانافزار StatusCodePages
app.UseStatusCodePages();
ج) با افزودن میانافزار صفحهی استثناءهای توسعه دهندهها
app.UseDeveloperExceptionPage();
امکان بازگشت سادهتر یک ProblemDetails سفارشی در دات نت 7
برای سفارشی سازی خروجی ProblemDetails، علاوه بر راهحلی که پیشتر در این مطلب مطرح شد، میتوان در دات نت 7 از روش تکمیلی ذیل نیز استفاده کرد:
builder.Services.AddProblemDetails(options => options.CustomizeProblemDetails = ctx => ctx.ProblemDetails.Extensions.Add("MachineName", Environment.MachineName));
الف) تعریف یک ErrorFeature سفارشی
public class MyErrorFeature { public ErrorType Error { get; set; } } public enum ErrorType { ArgumentException }
ب) تنظیم مقدار ErrorFeature سفارشی در اکشن متدها
[HttpGet("{value}")] public IActionResult MyErrorTest(int value) { if (value <= 0) { var errorType = new MyErrorFeature { Error = ErrorType.ArgumentException }; HttpContext.Features.Set(errorType); return BadRequest(); } return Ok(value); }
ج) واکنش نشان دادن به دریافت ErrorFeature سفارشی
services.AddProblemDetails(options => options.CustomizeProblemDetails = ctx => { var MyErrorFeature = ctx.HttpContext.Features.Get<MyErrorFeature>(); if (MyErrorFeature is not null) { (string Title, string Detail, string Type) details = MyErrorFeature.Error switch { ErrorType.ArgumentException => ( nameof(ArgumentException), "This is an argument-exception.", "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1" ), _ => ( nameof(Exception), "default-exception", "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1" ) }; ctx.ProblemDetails.Title = details.Title; ctx.ProblemDetails.Detail = details.Detail; ctx.ProblemDetails.Type = details.Type; } } );
امکان تبدیل سادهتر اطلاعات استثناءهای سفارشی به یک ProblemDetails سفارشی در دات نت 7
بجای استفاده از تنظیمات services.AddProblemDetails جهت بازنویسی مقدار شیء ProblemDetails بازگشتی، میتوان جزئیات میانافزار app.UseExceptionHandler را نیز سفارشی سازی کرد و به بروز استثناءهای خاصی واکنش نشان داد. برای مثال فرض کنید یک استثنای سفارشی را به صورت زیر طراحی کردهاید:
public class MyCustomException : Exception { public MyCustomException( string message, HttpStatusCode statusCode = HttpStatusCode.BadRequest ) : base(message) { StatusCode = statusCode; } public HttpStatusCode StatusCode { get; } }
[HttpGet("{value}")] public IActionResult MyErrorTest(int value) { if (value <= 0) { throw new MyCustomException("The value should be positive!"); } return Ok(value); }
app.UseExceptionHandler(exceptionHandlerApp => { exceptionHandlerApp.Run(async context => { context.Response.ContentType = "application/problem+json"; if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService) { var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>(); var exceptionType = exceptionHandlerFeature?.Error; if (exceptionType is not null) { (string Title, string Detail, string Type, int StatusCode) details = exceptionType switch { MyCustomException MyCustomException => ( exceptionType.GetType().Name, exceptionType.Message, "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1", context.Response.StatusCode = (int)MyCustomException.StatusCode ), _ => ( exceptionType.GetType().Name, exceptionType.Message, "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1", context.Response.StatusCode = StatusCodes.Status500InternalServerError ) }; await problemDetailsService.WriteAsync(new ProblemDetailsContext { HttpContext = context, ProblemDetails = { Title = details.Title, Detail = details.Detail, Type = details.Type, Status = details.StatusCode } }); } } }); });