من از این کنترل توی پروژهی ام وی سی خودم استفاده کردم این کنترل توی مرورگرهای کروم و IE به درستی کار میکنه
ولی توی فایرفاکس دکمه هاش کار نمیکنن و وقتی دکمه ایی فشار داده میشه کنترل بسته میشه
EF Code First #2
AspNetMVCNTierTemp-8659220f5c674a7ab86c531c19bc0837.7z
سرویس ما متدهای زیر را در دسترس قرار میدهد.
Relative URl | HTTP method | Action |
api/products/ | GET | گرفتن لیست تمام محصولات |
api/products/id/ | GET | گرفتن یک محصول بر اساس شناسه |
api/products?category=category/ | GET | گرفتن یک محصول بر اساس طبقه بندی |
api/products/ | POST | ایجاد یک محصول جدید |
api/products/id/ | PUT | بروز رسانی یک محصول |
api/products/id/ | DELETE | حذف یک محصول |
همانطور که مشاهده میکنید برخی از آدرس ها، شامل شناسه محصول هم میشوند. بعنوان مثال برای گرفتن محصولی با شناسه 28، کلاینت یک درخواست GET را به آدرس زیر ارسال میکند:
http://hostname/api/products/28
منابع
سرویس ما آدرس هایی برای دستیابی به دو نوع منبع (resource) را تعریف میکند:
URI | Resource |
api/products/ | لیست تمام محصولات |
api/products/id/ | یک محصول مشخص |
متد ها
چهار متد اصلی HTTP یعنی همان GET, PUT, POST, DELETE میتوانند بصورت زیر به عملیات CRUD نگاشت شوند:
- متد GET یک منبع (resource) را از آدرس تعریف شده دریافت میکند. متدهای GET هیچگونه تاثیری روی سرور نباید داشته باشند. مثلا حذف رکوردها با متد اکیدا اشتباه است.
- متد PUT یک منبع را در آدرس تعریف شده بروز رسانی میکند. این متد برای ساختن منابع جدید هم میتواند استفاده شود، البته در صورتی که سرور به کلاینتها اجازه مشخص کردن آدرسهای جدید را بدهد. در مثال جاری پشتیبانی از ایجاد منابع توسط متد PUT را بررسی نخواهیم کرد.
- متد POST منبع جدیدی میسازد. سرور آدرس آبجکت جدید را تعیین میکند و آن را بعنوان بخشی از پیام Response بر میگرداند.
- متد DELETE منبعی را در آدرس تعریف شده حذف میکند.
نکته: متد PUT موجودیت محصول (product entity) را کاملا جایگزین میکند. به بیان دیگر، از کلاینت انتظار میرود که آبجکت کامل محصول را برای بروز رسانی ارسال کند. اگر میخواهید از بروز رسانیهای جزئی/پاره ای (partial) پشتیبانی کنید متد PATCH توصیه میشود. مثال جاری متد PATCH را پیاده سازی نمیکند.
یک پروژه Web API جدید بسازید
ویژوال استودیو را باز کنید و پروژه جدیدی از نوع ASP.NET MVC Web Application بسازید. نام پروژه را به "ProductStore" تغییر دهید و OK کنید.
در دیالوگ New ASP.NET Project قالب Web API را انتخاب کرده و تایید کنید.
افزودن یک مدل
یک مدل، آبجکتی است که داده اپلیکیشن شما را نمایندگی میکند. در ASP.NET Web API میتوانید از آبجکتهای Strongly-typed بعنوان مدل هایتان استفاده کنید که بصورت خودکار برای کلاینت به فرمتهای JSON, XML مرتب (Serialize) میشوند. در مثال جاری، دادههای ما محصولات هستند. پس کلاس جدیدی بنام Product میسازیم.
در پوشه Models کلاس جدیدی با نام Product بسازید.
حال خواص زیر را به این کلاس اضافه کنید.
namespace ProductStore.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
افزودن یک مخزن
ما نیاز به ذخیره کردن کلکسیونی از محصولات داریم، و بهتر است این کلکسیون از پیاده سازی سرویس تفکیک شود. در این صورت بدون نیاز به بازنویسی کلاس سرویس میتوانیم منبع دادهها را تغییر دهیم. این نوع طراحی با نام الگوی مخزن یا Repository Pattern شناخته میشود. برای شروع نیاز به یک قرارداد جنریک برای مخزنها داریم.
روی پوشه Models کلیک راست کنید و گزینه Add, New Item را انتخاب نمایید.
نوع آیتم جدید را Interface انتخاب کنید و نام آن را به IProductRepository تغییر دهید.
حال کد زیر را به این اینترفیس اضافه کنید.
namespace ProductStore.Models { public interface IProductRepository { IEnumerable<Product> GetAll(); Product Get(int id); Product Add(Product item); void Remove(int id); bool Update(Product item); } }
namespace ProductStore.Models { public class ProductRepository : IProductRepository { private List<Product> products = new List<Product>(); private int _nextId = 1; public ProductRepository() { Add(new Product { Name = "Tomato soup", Category = "Groceries", Price = 1.39M }); Add(new Product { Name = "Yo-yo", Category = "Toys", Price = 3.75M }); Add(new Product { Name = "Hammer", Category = "Hardware", Price = 16.99M }); } public IEnumerable<Product> GetAll() { return products; } public Product Get(int id) { return products.Find(p => p.Id == id); } public Product Add(Product item) { if (item == null) { throw new ArgumentNullException("item"); } item.Id = _nextId++; products.Add(item); return item; } public void Remove(int id) { products.RemoveAll(p => p.Id == id); } public bool Update(Product item) { if (item == null) { throw new ArgumentNullException("item"); } int index = products.FindIndex(p => p.Id == item.Id); if (index == -1) { return false; } products.RemoveAt(index); products.Add(item); return true; } } }
مخزن ما لیست محصولات را در حافظه محلی نگهداری میکند. برای مثال جاری این طراحی کافی است، اما در یک اپلیکیشن واقعی دادههای شما در یک دیتابیس یا منبع داده ابری ذخیره خواهند شد. همچنین استفاده از الگوی مخزن، تغییر منبع دادهها در آینده را راحتتر میکند.
افزودن یک کنترلر Web API
اگر قبلا با ASP.NET MVC کار کرده باشید، با مفهوم کنترلرها آشنایی دارید. در ASP.NET Web API کنترلرها کلاس هایی هستند که درخواستهای HTTP دریافتی از کلاینت را به اکشن متدها نگاشت میکنند. ویژوال استودیو هنگام ساختن پروژه شما دو کنترلر به آن اضافه کرده است. برای مشاهد آنها پوشه Controllers را باز کنید.
- HomeController یک کنترلر مرسوم در ASP.NET MVC است. این کنترلر مسئول بکار گرفتن صفحات وب است و مستقیما ربطی به Web API ما ندارد.
- ValuesController یک کنترلر نمونه WebAPI است.
کنترلر ValuesController را حذف کنید، نیازی به این آیتم نخواهیم داشت. حال برای اضافه کردن کنترلری جدید مراحل زیر را دنبال کنید.
در پنجره Solution Explorer روی پوشه Controllers کلیک راست کرده و گزینه Add, Controller را انتخاب کنید.
در دیالوگ Add Controller نام کنترلر را به ProductsController تغییر داده و در قسمت Scaffolding Options گزینه Empty API Controller را انتخاب کنید.
حال فایل کنترلر جدید را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.
using ProductStore.Models;
public class ProductsController : ApiController { static readonly IProductRepository repository = new ProductRepository(); }
فراخوانی ()new ProductRepository طراحی جالبی نیست، چرا که کنترلر را به پیاده سازی بخصوصی از این اینترفیس گره میزند. بهتر است از تزریق وابستگی (Dependency Injection) استفاده کنید. برای اطلاعات بیشتر درباره تکنیک DI در Web API به این لینک مراجعه کنید.
گرفتن منابع
ProductStore API اکشنهای متعددی در قالب متدهای HTTP GET در دسترس قرار میدهد. هر اکشن به متدی در کلاس ProductsController مرتبط است.
Relative URl | HTTP Method | Action |
api/products/ | GET | دریافت لیست تمام محصولات |
api/products/id/ | GET | دریافت محصولی مشخص بر اساس شناسه |
api/products?category=category/ | GET | دریافت محصولات بر اساس طبقه بندی |
برای دریافت لیست تمام محصولات متد زیر را به کلاس ProductsController اضافه کنید.
public class ProductsController : ApiController { public IEnumerable<Product> GetAllProducts() { return repository.GetAll(); } // .... }
برای دریافت محصولی مشخص بر اساس شناسه آن متد زیر را اضافه کنید.
public Product GetProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return item; }
نام این متد هم با "Get" شروع میشود اما پارامتری با نام id دارد. این پارامتر به قسمت id مسیر درخواست شده (request URl) نگاشت میشود. تبدیل پارامتر به نوع داده مناسب (در اینجا int) هم بصورت خودکار توسط فریم ورک ASP.NET Web API انجام میشود.
متد GetProduct در صورت نامعتبر بودن پارامتر id استثنایی از نوع HttpResponseException تولید میکند. این استثنا بصورت خودکار توسط فریم ورک Web API به خطای 404 (Not Found) ترجمه میشود.
در آخر متدی برای دریافت محصولات بر اساس طبقه بندی اضافه کنید.
public IEnumerable<Product> GetProductsByCategory(string category) { return repository.GetAll().Where( p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); }
اگر آدرس درخواستی پارامترهای query string داشته باشد، Web API سعی میکند پارامترها را با پارامترهای متد کنترلر تطبیق دهد. بنابراین درخواستی به آدرس "api/products?category=category" به این متد نگاشت میشود.
ایجاد منبع جدید
قدم بعدی افزودن متدی به ProductsController برای ایجاد یک محصول جدید است. لیست زیر پیاده سازی ساده ای از این متد را نشان میدهد.
// Not the final implementation! public Product PostProduct(Product item) { item = repository.Add(item); return item; }
- نام این متد با "Post" شروع میشود. برای ساختن محصولی جدید کلاینت یک درخواست HTTP POST ارسال میکند.
- این متد پارامتری از نوع Product میپذیرد. در Web API پارامترهای پیچیده (complex types) بصورت خودکار با deserialize کردن بدنه درخواست بدست میآیند. بنابراین در اینجا از کلاینت انتظار داریم که آبجکتی از نوع Product را با فرمت XML یا JSON ارسال کند.
پیاده سازی فعلی این متد کار میکند، اما هنوز کامل نیست. در حالت ایده آل ما میخواهیم پیام HTTP Response موارد زیر را هم در بر گیرد:
- Response code: بصورت پیش فرض فریم ورک Web API کد وضعیت را به 200 (OK) تنظیم میکند. اما طبق پروتکل HTTP/1.1 هنگامی که یک درخواست POST منجر به ساخته شدن منبعی جدید میشود، سرور باید با کد وضعیت 201 (Created) پاسخ دهد.
- Location: هنگامی که سرور منبع جدیدی میسازد، باید آدرس منبع جدید را در قسمت Location header پاسخ درج کند.
ASP.NET Web API دستکاری پیام HTTP response را آسان میکند. لیست زیر پیاده سازی بهتری از این متد را نشان میدهد.
public HttpResponseMessage PostProduct(Product item) { item = repository.Add(item); var response = Request.CreateResponse<Product>(HttpStatusCode.Created, item); string uri = Url.Link("DefaultApi", new { id = item.Id }); response.Headers.Location = new Uri(uri); return response; }
متد CreateResponse آبجکتی از نوع HttpResponseMessage میسازد و بصورت خودکار آبجکت Product را مرتب (serialize) کرده و در بدنه پاسخ مینویسد. نکته دیگر آنکه مثال جاری، مدل را اعتبارسنجی نمیکند. برای اطلاعات بیشتر درباره اعتبارسنجی مدلها در Web API به این لینک مراجعه کنید.
بروز رسانی یک منبع
بروز رسانی یک محصول با PUT ساده است.
public void PutProduct(int id, Product product) { product.Id = id; if (!repository.Update(product)) { throw new HttpResponseException(HttpStatusCode.NotFound); } }
حذف یک منبع
برای حذف یک محصول متد زیر را به کلاس ProductsController اضافه کنید.
public void DeleteProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } repository.Remove(id); }
در مثال جاری متد DeleteProduct نوع void را بر میگرداند، که فریم ورک Web API آن را بصورت خودکار به کد وضعیت 204 (No Content) ترجمه میکند.
حذف موجودیتهای منفصل
فرض کنید موجودیتی را از یک سرویس WCF دریافت کرده اید و میخواهید آن را برای حذف علامت گذاری کنید. مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل ما صورت حسابها و پرداختهای متناظر را ارائه میکند. در اپلیکیشن جاری یک سرویس WCF پیاده سازی کرده ایم که عملیات دیتابیسی کلاینتها را مدیریت میکند. میخواهیم توسط این سرویس آبجکتی را (در اینجا یک موجودیت پرداخت) حذف کنیم. برای ساده نگاه داشتن مثال جاری، مدلها را در خود سرویس تعریف میکنیم. برای ایجاد سرویس مذکور مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع WCF Service Library بسازید و نام آن را به Recipe5 تغییر دهید.
- روی پروژه کلیک راست کنید و گزینه Add New Item را انتخاب کنید. سپس گزینههای Data -> ADO.NET Entity Data Model را برگزینید.
- از ویزارد ویژوال استودیو برای اضافه کردن یک مدل با جداول Invoice و Payment استفاده کنید. برای ساده نگه داشتن مثال جاری، فیلد پیمایشی Payments را از موجودیت Invoice حذف کرده ایم (برای این کار روی خاصیت پیمایشی Payments کلیک راست کنید و گزینه Delete From Model را انتخاب کنید.) روی خاصیت TimeStamp موجودیت Payment کلیک راست کنید و گزینه Properties را انتخاب کنید. سپس مقدار Concurrency Mode آن را به Fixed تغییر دهید. این کار باعث میشود که مقدار این فیلد برای کنترل همزمانی بررسی شود. بنابراین مقدار TimeStamp در عبارت WHERE تمام دستورات بروز رسانی و حذف درج خواهد شد.
- فایل IService1.cs را باز کنید و تعریف سرویس را مانند لیست زیر تغییر دهید.
[ServiceContract] public interface IService1 { [OperationContract] Payment InsertPayment(); [OperationContract] void DeletePayment(Payment payment); }
- فایل Service1.cs را باز کنید و پیاده سازی سرویس را مانند لیست زیر تغییر دهید.
public class Service1 : IService1 { public Payment InsertPayment() { using (var context = new EFRecipesEntities()) { // delete the previous test data context.Database.ExecuteSqlCommand("delete from [payments]"); context.Database.ExecuteSqlCommand("delete from [invoices]"); var payment = new Payment { Amount = 99.95M, Invoice = new Invoice { Description = "Auto Repair" } }; context.Payments.Add(payment); context.SaveChanges(); return payment; } } public void DeletePayment(Payment payment) { using (var context = new EFRecipesEntities()) { context.Entry(payment).State = EntityState.Deleted; context.SaveChanges(); } } }
- برای تست این سرویس به یک کلاینت نیاز داریم. یک پروژه جدید از نوع Console Application به راه حل جاری اضافه کنید و کد آن را مطابق لیست زیر تغییر دهید. فراموش نکنید که ارجاعی به سرویس هم اضافه کنید. روی پروژه کلاینت کلیک راست کرده و Add Service Reference را انتخاب نمایید. ممکن است پیش از آنکه بتوانید سرویس را ارجاع کنید، نیاز باشد پروژه سرویس را ابتدا اجرا کنید (کلیک راست روی پروژه سرویس و انتخاب گزینه Debug -> Start Instance).
class Program { static void Main() { var client = new Service1Client(); var payment = client.InsertPayment(); client.DeletePayment(payment); } }
شرح مثال جاری
در مثال جاری برای بروز رسانی و حذف موجودیتهای منفصل از الگویی رایج استفاده کرده ایم که در سرویسهای WCF و Web API استفاده میشود.
در کلاینت با فراخوانی متد InsertPayment یک پرداخت جدید در دیتابیس ذخیره میکنیم. این متد، موجودیت Payment ایجاد شده را باز میگرداند. موجودیتی که به کلاینت باز میگردد از DbContext منفصل (disconnected) است، در واقع در چنین وضعیتی آبجکت context ممکن است در فضای پروسس دیگری قرار داشته باشد، یا حتی روی کامپیوتر دیگری باشد.
برای حذف موجودیت Payment از متد DeletePayment استفاده میکنیم. این متد به نوبه خود با فراخوانی متد Entry روی آبجکت context و پاس دادن موجودیت پرداخت بعنوان آرگومان، موجودیت را پیدا میکند. سپس وضعیت موجودیت را به EntityState.Deleted تغییر میدهیم که این کار آبجکت را برای حذف علامت گذاری میکند. فراخوانیهای بعدی متد ()SaveChanges موجودیت را از دیتابیس حذف خواهد کرد.
آبجکت پرداختی که برای حذف به context الحاق کرده ایم تمام خاصیت هایش مقدار دهی شده اند، درست مانند هنگامی که این موجودیت به دیتابیس اضافه شده بود. اما از آنجا که از foreign key association استفاده میکنیم، تنها فیلدهای کلید موجودیت، خاصیت همزمانی (concurrency) و TimeStamp برای تولید عبارت where مناسب لازم هستند که نهایتا منجر به حذف موجودیت خواهد شد. تنها استثنا درباره این قاعده هنگامی است که موجودیت شما یک یا چند خاصیت از نوع پیچیده یا Complex Type داشته باشد. از آنجا که خاصیتهای پیچیده، اجزای ساختاری یک موجودیت محسوب میشوند نمیتوانند مقادیر null بپذیرند. یک راه حل ساده این است که هنگامی که EF مشغول ساختن عبارت SQL Delete لازم برای حذف موجودیت بر اساس کلید و خاصیت همزمانی آن است، وهله جدیدی از نوع داده پیچیده خود بسازید. اگر فیلدهای complex type را با مقادیر null رها کنید، فراخوانی متد ()SaveChanges با خطا مواجه خواهد شد.
اگر از یک independent association استفاده میکنید که در آن کثرت (multiplicity) موجودیت مربوطه یک، یا صفر به یک است، EF انتظار دارد که کلیدهای موجودیتها بدرستی مقدار دهی شوند تا بتواند عبارت where مناسب را برای دستورات بروز رسانی و حذف تولید کند. اگر در مثال جاری از یک رابطه independent association بین موجودیتهای Invoice و Payment استفاده میکردیم، لازم بود تا خاصیت پیمایشی Invoice را با وهله ای از صورت حساب مقدار دهی کنیم که خاصیت InvoiceId آن نیز بدرستی مقدار دهی شده باشد. در این صورت عبارت where نهایی شامل فیلدهای PaymentId, TimeStamp و InvoiceId خواهد بود.
نکته: هنگام پیاده سازی معماریهای n-Tier با Entity Framework، استفاده از رویکرد Foreign Key Association برای موجودیتهای مرتبط باید با ملاحظات جدی انجام شود. پیاده سازی رویکرد Independent Association مشکل است و میتواند کد شما را بسیار پیچیده کند. برای مطالعه بیشتر درباره این رویکردها و مزایا و معایب آنها به این لینک مراجعه کنید که توسط یکی از برنامه نویسان تیم EF نوشته شده است.
اگر موجودیت شما تعداد متعددی Independent Association دارد، مقدار دهی تمام آنها میتواند خسته کننده شود. رویکردی سادهتر این است که وهله مورد نظر را از دیتابیس دریافت کنید و آن را برای حذف علامت گذاری نمایید. این روش کد شما را سادهتر میکند، اما هنگامی که آبجکت را از دیتابیس دریافت میکنید EF کوئری جاری را بازنویسی میکند تا تمام روابط یک، یا صفر به یک بارگذاری شوند. مگر آنکه از گزینه NoTracking روی context خود استفاده کنید. اگر در مثال جاری رویکرد Independent Association را پیاده سازی کرده بودیم، هنگامی که موجودیت Payment را از دیتابیس دریافت میکنیم (قبل از علامت گذاری برای حذف) EF یک Object state entry برای موجودیت پرداخت و یک Relationship entry برای رابطه بین Payment و Invoice میساخت. سپس وقتی که موجودیت پرداخت را برای حذف علامت گذاری میکنیم، EF رابطه بین پرداخت و صورت حساب را هم برای حذف علامت گذاری میکند. در اینجا عبارت where تولید شده مانند قبل، شامل فیلدهای PaymentId, TimeStamp و InvoiceId خواهد بود.
یک گزینه دیگر برای حذف موجودیتها در Independent Associations این است که تمام موجودیتهای مرتبط را مشخصا بارگذاری کنیم (eager loading) و کل Object graph را برای حذف به سرویس WCF یا Web API بفرستیم. در مثال جاری میتوانستیم موجودیت صورتحساب مرتبط با موجودیت پرداخت را مشخصا بارگذاری کنیم. اگر میخواستیم موجودیت Payment را حذف کنیم، میتوانستیم کل گراف را که شامل هر دو موجودیت میشود به سرویس ارسال کنیم. اما هنگام استفاده از چنین روشی باید بسیار دقت کنید، چرا که این رویکرد پهنای باند بیشتری مصرف میکند و زمان پردازش بیشتری هم برای مرتب سازی (serialization) صرف میکند. بنابراین هزینه این رویکرد نسبت به سادگی کدی که بدست میآید به مراتب بیشتر است.
ASP.NET MVC #18
اعتبار سنجی کاربران در ASP.NET MVC
دو مکانیزم اعتبارسنجی کاربران به صورت توکار در ASP.NET MVC در دسترس هستند: Forms authentication و Windows authentication.
در حالت Forms authentication، برنامه موظف به نمایش فرم لاگین به کاربرها و سپس بررسی اطلاعات وارده توسط آنها است. برخلاف آن، Windows authentication حالت یکپارچه با اعتبار سنجی ویندوز است. برای مثال زمانیکه کاربری به یک دومین ویندوزی وارد میشود، از همان اطلاعات ورود او به شبکه داخلی، به صورت خودکار و یکپارچه جهت استفاده از برنامه کمک گرفته خواهد شد و بیشترین کاربرد آن در برنامههای نوشته شده برای اینترانتهای داخلی شرکتها است. به این ترتیب کاربران یک بار به دومین وارد شده و سپس برای استفاده از برنامههای مختلف ASP.NET، نیازی به ارائه نام کاربری و کلمه عبور نخواهند داشت. Forms authentication بیشتر برای برنامههایی که از طریق اینترنت به صورت عمومی و از طریق انواع و اقسام سیستم عاملها قابل دسترسی هستند، توصیه میشود (و البته منعی هم برای استفاده در حالت اینترانت ندارد).
ضمنا باید به معنای این دو کلمه هم دقت داشت: هدف از Authentication این است که مشخص گردد هم اکنون چه کاربری به سایت وارد شده است. Authorization، سطح دسترسی کاربر وارد شده به سیستم و اعمالی را که مجاز است انجام دهد، مشخص میکند.
فیلتر Authorize در ASP.NET MVC
یکی دیگر از فیلترهای امنیتی ASP.NET MVC به نام Authorize، کار محدود ساختن دسترسی به متدهای کنترلرها را انجام میدهد. زمانیکه اکشن متدی به این فیلتر یا ویژگی مزین میشود، به این معنا است که کاربران اعتبارسنجی نشده، امکان دسترسی به آنرا نخواهند داشت. فیلتر Authorize همواره قبل از تمامی فیلترهای تعریف شده دیگر اجرا میشود.
فیلتر Authorize با پیاده سازی اینترفیس System.Web.Mvc.IAuthorizationFilter توسط کلاس System.Web.Mvc.AuthorizeAttribute در دسترس میباشد. این کلاس علاوه بر پیاده سازی اینترفیس یاد شده، دارای دو خاصیت مهم زیر نیز میباشد:
public string Roles { get; set; } // comma-separated list of role names
public string Users { get; set; } // comma-separated list of usernames
زمانیکه فیلتر Authorize به تنهایی بکارگرفته میشود، هر کاربر اعتبار سنجی شدهای در سیستم قادر خواهد بود به اکشن متد مورد نظر دسترسی پیدا کند. اما اگر همانند مثال زیر، از خواص Roles و یا Users نیز استفاده گردد، تنها کاربران اعتبار سنجی شده مشخصی قادر به دسترسی به یک کنترلر یا متدی در آن خواهند شد:
[Authorize(Roles="Admins")]
public class AdminController : Controller
{
[Authorize(Users="Vahid")]
public ActionResult DoSomethingSecure()
{
}
}
در این مثال، تنها کاربرانی با نقش Admins قادر به دسترسی به کنترلر جاری Admin خواهند بود. همچنین در بین این کاربران ویژه، تنها کاربری به نام Vahid قادر است متد DoSomethingSecure را فراخوانی و اجرا کند.
اکنون سؤال اینجا است که فیلتر Authorize چگونه از دو مکانیزم اعتبار سنجی یاد شده استفاده میکند؟ برای پاسخ به این سؤال، فایل web.config برنامه را باز نموده و به قسمت authentication آن دقت کنید:
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
به صورت پیش فرض، برنامههای ایجاد شده توسط VS.NET جهت استفاده از حالت Forms یا همان Forms authentication تنظیم شدهاند. در اینجا کلیه کاربران اعتبار سنجی نشده، به کنترلری به نام Account و متد LogOn در آن هدایت میشوند.
برای تغییر آن به حالت اعتبار سنجی یکپارچه با ویندوز، فقط کافی است مقدار mode را به Windows تغییر داد و تنظیمات forms آنرا نیز حذف کرد.
یک نکته: اعمال تنظیمات اعتبار سنجی اجباری به تمام صفحات سایت
تنظیم زیر نیز در فایل وب کانفیگ برنامه، همان کار افزودن ویژگی Authorize را انجام میدهد با این تفاوت که تمام صفحات سایت را به صورت خودکار تحت پوشش قرار خواهد داد (البته منهای loginUrl ایی که در تنظیمات فوق مشاهده نمودید):
<authorization>
<deny users="?" />
</authorization>
در این حالت دسترسی به تمام آدرسهای سایت تحت تاثیر قرار میگیرند، منجمله دسترسی به تصاویر و فایلهای CSS و غیره. برای اینکه این موارد را برای مثال در حین نمایش صفحه لاگین نیز نمایش دهیم، باید تنظیم زیر را پیش از تگ system.web به فایل وب کانفیگ برنامه اضافه کرد:
<!-- we don't want to stop anyone seeing the css and images -->
<location path="Content">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
در اینجا پوشه Content از سیستم اعتبارسنجی اجباری خارج میشود و تمام کاربران به آن دسترسی خواهند داشت.
به علاوه امکان امن ساختن تنها قسمتی از سایت نیز میسر است؛ برای مثال:
<location path="secure">
<system.web>
<authorization>
<allow roles="Administrators" />
<deny users="*" />
</authorization>
</system.web>
</location>
در اینجا مسیری به نام secure، نیاز به اعتبارسنجی اجباری دارد. به علاوه تنها کاربرانی در نقش Administrators به آن دسترسی خواهند داشت.
نکته: به تنظیمات انجام شده در فایل Web.Config دقت داشته باشید
همانطور که میشود دسترسی به یک مسیر را توسط تگ location بازگذاشت، امکان بستن آن هم فراهم است (بجای allow از deny استفاده شود). همچنین در ASP.NET MVC به سادگی میتوان تنظیمات مسیریابی را در فایل global.asax.cs تغییر داد. برای مثال اینبار مسیر دسترسی به صفحات امن سایت، Admin خواهد بود نه Secure. در این حالت چون از فیلتر Authorize استفاده نشده و همچنین فایل web.config نیز تغییر نکرده، این صفحات بدون محافظت رها خواهند شد.
بنابراین اگر از تگ location برای امن سازی قسمتی از سایت استفاده میکنید، حتما باید پس از تغییرات مسیریابی، فایل web.config را هم به روز کرد تا به مسیر جدید اشاره کند.
به همین جهت در ASP.NET MVC بهتر است که صریحا از فیلتر Authorize بر روی کنترلرها (جهت اعمال به تمام متدهای آن) یا بر روی متدهای خاصی از کنترلرها استفاده کرد.
امکان تعریف AuthorizeAttribute در فایل global.asax.cs و متد RegisterGlobalFilters آن به صورت سراسری نیز وجود دارد. اما در این حالت حتی صفحه لاگین سایت هم دیگر در دسترس نخواهد بود. برای رفع این مشکل در ASP.NET MVC 4 فیلتر دیگری به نام AllowAnonymousAttribute معرفی شده است تا بتوان قسمتهایی از سایت را مانند صفحه لاگین، از سیستم اعتبارسنجی اجباری خارج کرد تا حداقل کاربر بتواند نام کاربری و کلمه عبور خودش را وارد نماید:
[System.Web.Mvc.AllowAnonymous]
public ActionResult Login()
{
return View();
}
بنابراین در ASP.NET MVC 4.0، فیلتر AuthorizeAttribute را سراسری تعریف کنید. سپس در کنترلر لاگین برنامه از فیلتر AllowAnonymous استفاده نمائید.
البته نوشتن فیلتر سفارشی AllowAnonymousAttribute در ASP.NET MVC 3.0 نیز میسر است. برای مثال:
public class LogonAuthorize : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext) {
if (!(filterContext.Controller is AccountController))
base.OnAuthorization(filterContext);
}
}
در این فیلتر سفارشی، اگر کنترلر جاری از نوع AccountController باشد، از سیستم اعتبار سنجی اجباری خارج خواهد شد. مابقی کنترلرها همانند سابق پردازش میشوند. به این معنا که اکنون میتوان LogonAuthorize را به صورت یک فیلتر سراسری در فایل global.asax.cs معرفی کرد تا به تمام کنترلرها، منهای کنترلر Account اعمال شود.
مثالی جهت بررسی حالت Windows Authentication
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس یک کنترلر جدید را به نام Home نیز به آن اضافه کنید. در ادامه متد Index آنرا با ویژگی Authorize، مزین نمائید. همچنین بر روی نام این متد کلیک راست کرده و یک View خالی را برای آن ایجاد کنید:
using System.Web.Mvc;
namespace MvcApplication15.Controllers
{
public class HomeController : Controller
{
[Authorize]
public ActionResult Index()
{
return View();
}
}
}
محتوای View متناظر با متد Index را هم به شکل زیر تغییر دهید تا نام کاربر وارد شده به سیستم را نمایش دهد:
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
Current user: @User.Identity.Name
به علاوه در فایل Web.config برنامه، حالت اعتبار سنجی را به ویندوز تغییر دهید:
<authentication mode="Windows" />
اکنون اگر برنامه را اجرا کنید و وب سرور آزمایشی انتخابی هم IIS Express باشد، پیغام HTTP Error 401.0 - Unauthorized نمایش داده میشود. علت هم اینجا است که Windows Authentication به صورت پیش فرض در این وب سرور غیرفعال است. برای فعال سازی آن به مسیر My Documents\IISExpress\config مراجعه کرده و فایل applicationhost.config را باز نمائید. تگ windowsAuthentication را یافته و ویژگی enabled آنرا که false است به true تنظیم نمائید. اکنون اگر برنامه را مجددا اجرا کنیم، در محل نمایش User.Identity.Name، نام کاربر وارد شده به سیستم نمایش داده خواهد شد.
همانطور که مشاهده میکنید در اینجا همه چیز یکپارچه است و حتی نیازی نیست صفحه لاگین خاصی را به کاربر نمایش داد. همینقدر که کاربر توانسته به سیستم ویندوزی وارد شود، بر این اساس هم میتواند از برنامههای وب موجود در شبکه استفاده کند.
بررسی حالت Forms Authentication
برای کار با Forms Authentication نیاز به محلی برای ذخیره سازی اطلاعات کاربران است. اکثر مقالات را که مطالعه کنید شما را به مباحث membership مطرح شده در زمان ASP.NET 2.0 ارجاع میدهند. این روش در ASP.NET MVC هم کار میکند؛ اما الزامی به استفاده از آن نیست.
برای بررسی حالت اعتبار سنجی مبتنی بر فرمها، یک برنامه خالی ASP.NET MVC جدید را آغاز کنید. یک کنترلر Home ساده را نیز به آن اضافه نمائید.
سپس نیاز است نکته «تنظیمات اعتبار سنجی اجباری تمام صفحات سایت» را به فایل وب کانفیگ برنامه اعمال نمائید تا نیازی نباشد فیلتر Authorize را در همه جا معرفی کرد. سپس نحوه معرفی پیش فرض Forms authentication تعریف شده در فایل web.config نیز نیاز به اندکی اصلاح دارد:
<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403MyApp"
cookieless="UseCookies"
loginUrl="~/Account/LogOn"
defaultUrl="~/Home"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200"/>
</authentication>
در اینجا استفاده از کوکیها اجباری شده است. loginUrl به کنترلر و متد لاگین برنامه اشاره میکند. defaultUrl مسیری است که کاربر پس از لاگین به صورت خودکار به آن هدایت خواهد شد. همچنین نکتهی مهم دیگری را که باید رعایت کرد، name ایی است که در این فایل config عنوان میکنید. اگر بر روی یک وب سرور، چندین برنامه وب ASP.Net را در حال اجرا دارید، باید برای هر کدام از اینها نامی جداگانه و منحصربفرد انتخاب کنید، در غیراینصورت تداخل رخ داده و گزینه مرا به خاطر بسپار شما کار نخواهد کرد.
کار slidingExpiration که در اینجا تنظیم شده است نیز به صورت زیر میباشد:
اگر لاگین موفقیت آمیزی ساعت 5 عصر صورت گیرد و timeout شما به عدد 10 تنظیم شده باشد، این لاگین به صورت خودکار در 5:10 منقضی خواهد شد. اما اگر در این حین در ساعت 5:05 ، کاربر، یکی از صفحات سایت شما را مرور کند، زمان منقضی شدن کوکی ذکر شده به 5:15 تنظیم خواهد شد(مفهوم تنظیم slidingExpiration). لازم به ذکر است که اگر کاربر پیش از نصف زمان منقضی شدن کوکی (مثلا در 5:04)، یکی از صفحات را مرور کند، تغییری در این زمان نهایی منقضی شدن رخ نخواهد داد.
اگر timeout ذکر نشود، زمان منقضی شدن کوکی ماندگار (persistent) مساوی زمان جاری + زمان منقضی شدن سشن کاربر که پیش فرض آن 30 دقیقه است، خواهد بود.
سپس یک مدل را به نام Account به پوشه مدلهای برنامه با محتوای زیر اضافه نمائید:
using System.ComponentModel.DataAnnotations;
namespace MvcApplication15.Models
{
public class Account
{
[Required(ErrorMessage = "Username is required to login.")]
[StringLength(20)]
public string Username { get; set; }
[Required(ErrorMessage = "Password is required to login.")]
[DataType(DataType.Password)]
public string Password { get; set; }
public bool RememberMe { get; set; }
}
}
همچنین مطابق تنظیمات اعتبار سنجی مبتنی بر فرمهای فایل وب کانفیگ، نیاز به یک AccountController نیز هست:
using System.Web.Mvc;
using MvcApplication15.Models;
namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn()
{
return View();
}
[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
return View();
}
}
}
در اینجا در حالت HttpGet فرم لاگین نمایش داده خواهد شد. بنابراین بر روی این متد کلیک راست کرده و گزینه Add view را انتخاب کنید. سپس در صفحه باز شده گزینه Create a strongly typed view را انتخاب کرده و مدل را هم بر روی کلاس Account قرار دهید. قالب scaffolding را هم Create انتخاب کنید. به این ترتیب فرم لاگین برنامه ساخته خواهد شد.
اگر به متد HttpPost فوق دقت کرده باشید، علاوه بر دریافت وهلهای از شیء Account، یک رشته را به نام returnUrl نیز تعریف کرده است. علت هم اینجا است که سیستم Forms authentication، صفحه بازگشت را به صورت خودکار به شکل یک کوئری استرینگ به انتهای Url جاری اضافه میکند. مثلا:
http://localhost/Account/LogOn?ReturnUrl=something
بنابراین اگر یکی از پارامترهای متد تعریف شده به نام returnUrl باشد، به صورت خودکار مقدار دهی خواهد شد.
تا اینجا زمانیکه برنامه را اجرا کنیم، ابتدا بر اساس تعاریف مسیریابی پیش فرض برنامه، آدرس کنترلر Home و متد Index آن فراخوانی میگردد. اما چون در وب کانفیگ برنامه authorization را فعال کردهایم، برنامه به صورت خودکار به آدرس مشخص شده در loginUrl قسمت تعاریف اعتبارسنجی مبتنی بر فرمها هدایت خواهد شد. یعنی آدرس کنترلر Account و متد LogOn آن درخواست میگردد. در این حالت صفحه لاگین نمایان خواهد شد.
مرحله بعد، اعتبار سنجی اطلاعات وارد شده کاربر است. بنابراین نیاز است کنترلر Account را به نحو زیر بازنویسی کرد:
using System.Web.Mvc;
using System.Web.Security;
using MvcApplication15.Models;
namespace MvcApplication15.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public ActionResult LogOn(string returnUrl)
{
if (User.Identity.IsAuthenticated) //remember me
{
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect(FormsAuthentication.DefaultUrl);
}
return View(); // show the login page
}
[HttpGet]
public void LogOut()
{
FormsAuthentication.SignOut();
}
private bool shouldRedirect(string returnUrl)
{
// it's a security check
return !string.IsNullOrWhiteSpace(returnUrl) &&
Url.IsLocalUrl(returnUrl) &&
returnUrl.Length > 1 &&
returnUrl.StartsWith("/") &&
!returnUrl.StartsWith("//") &&
!returnUrl.StartsWith("/\\");
}
[HttpPost]
public ActionResult LogOn(Account loginInfo, string returnUrl)
{
if (this.ModelState.IsValid)
{
if (loginInfo.Username == "Vahid" && loginInfo.Password == "123")
{
FormsAuthentication.SetAuthCookie(loginInfo.Username, loginInfo.RememberMe);
if (shouldRedirect(returnUrl))
{
return Redirect(returnUrl);
}
FormsAuthentication.RedirectFromLoginPage(loginInfo.Username, loginInfo.RememberMe);
}
}
this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!";
return View(loginInfo);
}
}
}
در اینجا با توجه به گزینه «مرا به خاطر بسپار»، اگر کاربری پیشتر لاگین کرده و کوکی خودکار حاصل از اعتبار سنجی مبتنی بر فرمهای او نیز معتبر باشد، مقدار User.Identity.IsAuthenticated مساوی true خواهد بود. بنابراین نیاز است در متد LogOn از نوع HttpGet به این مساله دقت داشت و کاربر اعتبار سنجی شده را به صفحه پیشفرض تعیین شده در فایل web.config برنامه یا returnUrl هدایت کرد.
در متد LogOn از نوع HttpPost، کار اعتبارسنجی اطلاعات ارسالی به سرور انجام میشود. در اینجا فرصت خواهد بود تا اطلاعات دریافتی، با بانک اطلاعاتی مقایسه شوند. اگر اطلاعات مطابقت داشتند، ابتدا کوکی خودکار FormsAuthentication تنظیم شده و سپس به کمک متد RedirectFromLoginPage کاربر را به صفحه پیش فرض سیستم هدایت میکنیم. یا اگر returnUrl ایی وجود داشت، آنرا پردازش خواهیم کرد.
برای پیاده سازی خروج از سیستم هم تنها کافی است متد FormsAuthentication.SignOut فراخوانی شود تا تمام اطلاعات سشن و کوکیهای مرتبط، به صورت خودکار حذف گردند.
تا اینجا فیلتر Authorize بدون پارامتر و همچنین در حالت مشخص سازی صریح کاربران به نحو زیر را پوشش دادیم:
[Authorize(Users="Vahid")]
اما هنوز حالت استفاده از Roles در فیلتر Authorize باقی مانده است. برای فعال سازی خودکار بررسی نقشهای کاربران نیاز است یک Role provider سفارشی را با پیاده سازی کلاس RoleProvider، طراحی کنیم. برای مثال:
using System;
using System.Web.Security;
namespace MvcApplication15.Helper
{
public class CustomRoleProvider : RoleProvider
{
public override bool IsUserInRole(string username, string roleName)
{
if (username.ToLowerInvariant() == "ali" && roleName.ToLowerInvariant() == "User")
return true;
// blabla ...
return false;
}
public override string[] GetRolesForUser(string username)
{
if (username.ToLowerInvariant() == "ali")
{
return new[] { "User", "Helpdesk" };
}
if(username.ToLowerInvariant()=="vahid")
{
return new [] { "Admin" };
}
return new string[] { };
}
public override void AddUsersToRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}
public override string ApplicationName
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
}
public override void CreateRole(string roleName)
{
throw new NotImplementedException();
}
public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
{
throw new NotImplementedException();
}
public override string[] FindUsersInRole(string roleName, string usernameToMatch)
{
throw new NotImplementedException();
}
public override string[] GetAllRoles()
{
throw new NotImplementedException();
}
public override string[] GetUsersInRole(string roleName)
{
throw new NotImplementedException();
}
public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
{
throw new NotImplementedException();
}
public override bool RoleExists(string roleName)
{
throw new NotImplementedException();
}
}
}
در اینجا حداقل دو متد IsUserInRole و GetRolesForUser باید پیاده سازی شوند و مابقی اختیاری هستند.
بدیهی است در یک برنامه واقعی این اطلاعات باید از یک بانک اطلاعاتی خوانده شوند؛ برای نمونه به ازای هر کاربر تعدادی نقش وجود دارد. به ازای هر نقش نیز تعدادی کاربر تعریف شده است (یک رابطه many-to-many باید تعریف شود).
در مرحله بعد باید این Role provider سفارشی را در فایل وب کانفیگ برنامه در قسمت system.web آن تعریف و ثبت کنیم:
<roleManager>
<providers>
<clear />
<add name="CustomRoleProvider" type="MvcApplication15.Helper.CustomRoleProvider"/>
</providers>
</roleManager>
همین مقدار برای راه اندازی بررسی نقشها در ASP.NET MVC کفایت میکند. اکنون امکان تعریف نقشها، حین بکارگیری فیلتر Authorize میسر است:
[Authorize(Roles = "Admin")]
public class HomeController : Controller
تسکهای پس زمینه (Background Job) چیست؟
سؤال : HangFire چیست؟
مقایسه HangFire و Quartz
دیتابیس :
ساختار :
داشبورد :
استفاده از HangFire
1. نصب :
- برای نصب HangFire در یک پروژهی Asp.Net Core لازم است ابتدا پکیجهای مورد نیاز آن را نصب کنید؛ که شامل موارد زیر است:
Install-Package Hangfire.Core Install-Package Hangfire.SqlServer Install-Package Hangfire.AspNetCore
- پس از نصب پکیجها باید تنظیمات مورد نیاز برای پیاده سازی HangFire را در برنامه، اعمال کنیم. این تنظیمات شامل افزودن سرویسها و اینترفیسهای HangFire به برنامه است که اینکار را با افزودن HangFire به متد ConfigureService کلاس Startup انجام خواهیم داد:
public void ConfigureServices(IServiceCollection services) { // Add Hangfire services. services.AddHangfire(configuration => configuration .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions { CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.Zero, UseRecommendedIsolationLevel = true, DisableGlobalLocks = true })); // Add the processing server as IHostedService services.AddHangfireServer(); // Add framework services. services.AddMvc(); }
- پکیج HangFire برای مدیریت کار و زمان ، Table هایی دارد که پس از نصب، بر روی دیتابیس برنامهی شما قرار میگیرد. فقط باید دقت داشته باشید ConnectionString دیتابیس خود را در متد AddHangFire مقدار دهی کنید، تا از این طریق دیتابیس برنامه را شناخته و Tableهای مورد نظر را در Schema جدیدی با نام HangFire به آن اضافه کند.
- این پکیج یک داشبورد اختصاصی دارد که در آن لیستی از انواع تسکهای در صف انجام و گزارشی از انجام شدهها را در اختیار ما قرار میدهد. برای تنظیم این داشبورد باید Middleware مربوط به آن و endpoint جدیدی را برای شناسایی مسیر داشبورد HangFire در برنامه، در متد Configure کلاس Startup اضافه کنید :
public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IHostingEnvironment env) { // HangFire Dashboard app.UseHangfireDashboard(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); // HangFire Dashboard endpoint endpoints.MapHangfireDashboard(); }); }
http://localhost:50255/hangfire
app.UseHangfireDashboard("/mydashboard");
http://localhost:50255/mydashboard
2. داشبورد :
3. امنیت داشبورد :
public class MyAuthorizationFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) { var httpContext = context.GetHttpContext(); // Allow all authenticated users to see the Dashboard (potentially dangerous). return httpContext.User.Identity.IsAuthenticated; } }
app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new [] { new MyAuthorizationFilter() } });
app.UseHangfireDashboard("/hangfire", new DashboardOptions { IsReadOnlyFunc = (DashboardContext context) => true });
انواع تسکها در HangFire :
1. تسکهای Fire-And-Forget :
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = backgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName) { var jobId = _backgroundJobClient.Enqueue(() => SendWelcomeMail(userName)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
2. تسکهای Delayed :
- دسته اول : اجرا پس از تاخیر در زمانی مشخص.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = backgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName) { var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),TimeSpan.FromMinutes(10)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
- دسته دوم : اجرا در زمانی مشخص.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = IBackgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName , DateTime dateAndTime) { var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),DateTimeOffset(dateAndTime)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
3. تسکهای Recurring :
public class HomeController : Controller { private readonly IRecurringJobManager _recurringJobManager; public HomeController(IRecurringJobManager recurringJobManager) { _recurringJobManager = recurringJobManager; } [HttpPost] [Route("BackUp")] public IActionResult BackUp(string userName) { _recurringJobManager.AddOrUpdate("test", () => BackUpDataBase(), Cron.Weekly); return Ok(); } public void BackUpDataBase() { // ... } }
- هر دقیقه (Cron.Minutely) :
_recurringJobManager.AddOrUpdate("test", () => job , Cron.Minutely);
- هر ساعت (Cron.Hourly) :
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly(10));
- هر روز (Cron.Daily) :
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily(3,10));
- هر هفته (Cron.Weekly) :
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Weekly);
_recurringJobManager.AddOrUpdate("test", () => Job,Cron.Weekly(DayOfWeek.Monday,3,10));
- هر ماه (Cron.Monthly) :
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly(10,3,10));
- هر سال (Cron.Yearly) :
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly(2,4,3,10));
4. تسکهای Continuations :
var stepOne = _backgroundJobClient.Schedule(() => SendAuthorizationEmail(), TimeSpan.FromMinute(10)); _backgroundJobClient.ContinueJobWith(stepOne, () => _backgroundJobClient.Schedule(() => ExpireAuthorizationEmail(), TimeSpan.FromHours(5)));
برخی از نکات و ترفندهای HangFire
1. استفاده از Cron Expression در Recurring Jobها :
* * * * * command to be executed - - - - - | | | | | | | | | ----- Day of week (0 - 7) (Sunday=0 or 7) | | | ------- Month (1 - 12) | | --------- Day of month (1 - 31) | ----------- Hour (0 - 23) ------------- Minute (0 - 59)
* * * * *
- فیلد اول (Minute) : در این فیلد باید دقیقهای مشخص از یک ساعت را وارد کنید؛ مانند دقیقه 10 (میتوانید محدودهای را هم تعیین کنید)
- فیلد دوم (Hour) : در این فیلد باید زمان معلومی را با فرمت ساعت وارد کنید؛ مانند ساعت 7 (میتوانید محدودهای را هم تعیین کنید، مانند ساعات 12-7)
- فیلد سوم (Day of Month) : در این فیلد باید یک روز از ماه را وارد کنید؛ مانند روز 15 ام از ماه (میتوانید محدودهای را هم تعیین کنید)
- فیلد چهارم (Month) : در این فیلد باید یک ماه از سال را وارد کنید؛ مثلا ماه 4 ام(آوریل) (میتوانید محدودهای را هم تعیین کنید)
- فیلد پنجم (Day of Week) : در این فیلد باید روزی از روزهای هفته یا محدودهای از آن روزها را تعیین کنید. مانند صفرم هفته که در کشورهای اروپایی و آمریکایی معادل روز یکشنبه است.
0 4 10-15 * *
_recurringJobManager.AddOrUpdate("test", () => job , "0 4 10-15 * *" );
2. متد Trigger :
RecurringJob.Trigger("some-id");
3. تعیین تاریخ انقضاء برای Recurring Jobها :
_recurringJobManager.AddOrUpdate("test", () => Console.WriteLine("Recurring Job"), Cron.Monthly); _backgroundJobClient.Schedule(() => _recurringJobManager.RemoveIfExists("test"), DateTimeOffset(dateAndTime));
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت سوم - تکمیل مستندات یک API با کامنتها
استفاده از XML Comments برای بهبود کیفیت مستندات API
نوشتن توضیحات XML ای برای متدها و پارامترها در پروژههای داتنتی، روشی استاندارد و شناخته شدهاست. برای نمونه در AuthorsController، میخواهیم توضیحاتی را به اکشن متد GetAuthor آن اضافه کنیم:
/// <summary> /// Get an author by his/her id /// </summary> /// <param name="authorId">The id of the author you want to get</param> /// <returns>An ActionResult of type Author</returns> [HttpGet("{authorId}")] public async Task<ActionResult<Author>> GetAuthor(Guid authorId)
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup>
اکنون نیاز است وجود این فایل را به تنظیمات SwaggerDoc در کلاس Startup برنامه، اعلام کنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(setupAction => { setupAction.SwaggerDoc( // ... ); var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile); setupAction.IncludeXmlComments(xmlCommentsFullPath); }); }
پس از این تنظیمات اگر برنامه را اجرا کنیم، در Swagger-UI حاصل، این تغییرات قابل مشاهده هستند:
افزودن توضیحات به Response
تا اینجا توضیحات پارامترها و متدها را افزودیم؛ اما response از نوع 200 آن هنوز فاقد توضیحات است:
علت را نیز در تصویر فوق مشاهده میکنید. قسمت responses در OpenAPI specification، اطلاعات خودش را از اسکیمای مدلهای مرتبط دریافت میکند. بنابراین نیاز است کلاس DTO متناظر با Author را به نحو ذیل تکمیل کنیم:
using System; namespace OpenAPISwaggerDoc.Models { /// <summary> /// An author with Id, FirstName and LastName fields /// </summary> public class Author { /// <summary> /// The id of the author /// </summary> public Guid Id { get; set; } /// <summary> /// The first name of the author /// </summary> public string FirstName { get; set; } /// <summary> /// The last name of the author /// </summary> public string LastName { get; set; } } }
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project>
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(setupAction => { setupAction.SwaggerDoc( // ... ); var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml", SearchOption.TopDirectoryOnly).ToList(); xmlFiles.ForEach(xmlFile => setupAction.IncludeXmlComments(xmlFile)); }); }
در این حالت اگر مجددا برنامه را اجرا کنیم، خروجی ذیل را در قسمت schemas مشاهده خواهیم کرد:
بهبود مستندات به کمک Data Annotations
اگر به اکشن متد UpdateAuthor در کنترلر نویسندگان دقت کنیم، چنین امضایی را دارد:
[HttpPut("{authorId}")] public async Task<ActionResult<Author>> UpdateAuthor(Guid authorId, AuthorForUpdate authorForUpdate)
using System.ComponentModel.DataAnnotations; namespace OpenAPISwaggerDoc.Models { /// <summary> /// An author for update with FirstName and LastName fields /// </summary> public class AuthorForUpdate { /// <summary> /// The first name of the author /// </summary> [Required] [MaxLength(150)] public string FirstName { get; set; } /// <summary> /// The last name of the author /// </summary> [Required] [MaxLength(150)] public string LastName { get; set; } } }
بهبود مستندات متد HttpPatch با ارائهی یک مثال
دو نگارش از اکشن متد UpdateAuthor در این مثال موجود هستند:
یکی HttpPut است
[HttpPut("{authorId}")] public async Task<ActionResult<Author>> UpdateAuthor(Guid authorId, AuthorForUpdate authorForUpdate)
[HttpPatch("{authorId}")] public async Task<ActionResult<Author>> UpdateAuthor( Guid authorId, JsonPatchDocument<AuthorForUpdate> patchDocument)
بهتر است در این حالت مثالی را به استفاده کنندگان از آن ارائه دهیم تا در حین کار با آن، به مشکل برنخورند:
/// <summary> /// Partially update an author /// </summary> /// <param name="authorId">The id of the author you want to get</param> /// <param name="patchDocument">The set of operations to apply to the author</param> /// <returns>An ActionResult of type Author</returns> /// <remarks> /// Sample request (this request updates the author's first name) \ /// PATCH /authors/id \ /// [ \ /// { \ /// "op": "replace", \ /// "path": "/firstname", \ /// "value": "new first name" \ /// } \ /// ] \ /// </remarks> [HttpPatch("{authorId}")] public async Task<ActionResult<Author>> UpdateAuthor( Guid authorId, JsonPatchDocument<AuthorForUpdate> patchDocument)
روش کنترل warningهای کامنتهای تکمیل نشده
با فعالسازی GenerateDocumentationFile در فایل csproj برنامه، کامپایلر، بلافاصله برای تمام متدها و خواص عمومی که دارای کامنت نیستند، یک warning را صادر میکند. یک روش برطرف کردن این مشکل، افزودن کامنت به تمام قسمتهای برنامه است. روش دیگر آن، تکمیل خواص کامپایلر، جهت مواجه شدن با عدم وجود کامنتها در فایل csproj برنامه است:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>false</TreatWarningsAsErrors> <WarningsAsErrors>NU1605;</WarningsAsErrors> <NoWarn>1701;1702;1591</NoWarn> </PropertyGroup>
- اگر میخواهید خودتان را مجبور به کامنت نویسی کنید، میتوانید نبود کامنتها را تبدیل به error کنید. برای این منظور خاصیت TreatWarningsAsErrors را به true تنظیم کنید. در این حالت هر کامنت نوشته نشده، به صورت یک error توسط کامپایلر گوشزد شده و برنامه کامپایل نخواهد شد.
- اگر TreatWarningsAsErrors را خاموش کردید، هنوز هم میتوانید یکسری از warningهای انتخابی را تبدیل به error کنید. برای مثال NU1605 ذکر شدهی در خاصیت WarningsAsErrors، مربوط به package downgrade detection warning است.
- اگر به warning نبود کامنتها دقت کنیم به صورت عبارات warning CS1591: Missing XML comment for publicly visible type or member شروع میشود. یعنی CS1591 مربوط به کامنتهای نوشته نشدهاست. میتوان برای صرفنظر کردن از آن، شمارهی این خطا را بدون CS، توسط خاصیت NoWarn ذکر کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-03.zip
در قسمت بعد، مشکل خروجی تولید response از نوع 200 را که در قسمت دوم به آن اشاره کردیم، بررسی خواهیم کرد.