مطالب
رویه های ذخیره شده خوب یا بد؟!

استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجه‌ی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخه‌ی خاص، برای همه‌ی سناریو‌ها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقی‌تر مینمایند.

Stored Procedure (که از این به بعد برای ایجاز، SP نوشته خواهد شد) هم از قاعده فوق مستثنی نیست و در صورت انتخاب صحیح میتواند به ارائه‌ی محصول نهایی با کیفیت‌تری در زمان کوتاه‌تری کمک کند و در صورت انتخاب ناآگاهانه ممکن است باعث شکست یک پروژه (بخصوص در بلند مدت) شود.


تاریخچه

SQL توسط شرکت IBM در اوایل دهه 70 میلادی ایجاد شد. با اوج گرفتن زبان‌های رویه‌ای، SQL هم چندان از این قافله عقب نماند که منجر به پذیرش SP به عنوان یک استاندارد، در دهه 90 میلادی و پیاده سازی تدریجی آن توسط غول‌های سازنده دیتابیس شد (رجوع فرمایید به ^ و ^). این فاصله 20 ساله باعث غنی‌تر شدن SQL شد و وجود SP - به معنی انتقال مدل برنامه نویسی رویه‌ای به SQL - بخشی از مشکلات قبلی کار با کوئری‌های پشت سر هم و خام را حل کرد. از سال 2000 میلادی به بعد، ORM‌های قدرتمندی از جمله  Hibernate  و پیاده سازی‌های مختلفی از Active Record  و Entity Framework متولد شدند. بنابر این تقدم و تاخّرهای زمانی، بدیهی است اغلب مزایای SP نسبت به Raw SQL Query و اغلب معایب آن نسبت به ORM‌ها باشد. 

بنظر میرسد برای پاسخ به سوال اصلی این مطلب، ناگزیر به مقایسه SP با رقبای دیرینه‌اش هستیم. با برشمردن معایب و مزایای SP میتوان به نتیجه‌ی منطقی‌تری رسید. البته باید در نظر داشت صرف استفاده از SP به معنای بهره‌مند شدن از مزایای آن و صرف استفاده نکردن از آن هم بهره‌مندی از رقبای آن نیست. چگونگی استفاده یک ابزار، مهمتر از خود ابزار است.


معایب SP

- دستورات Alter Table ، Add Column و Drop Column  به این سادگی‌ها هم نیستند؛ ممکن است به یکی از جداول دیتابیس دو ستون اضافه یا از آن حذف شوند. مجبوریم تمامی SP‌ها را بخصوص Insert و Update متناظر با جدول را تغییر دهیم که این تغییرات ممکن است بصورت زنجیره‌وار به سایر SP‌ها هم سرایت کند. حال شرایطی را در نظر بگیرید که تعداد SP‌های شما به چند ده و یا حتی به چند صد عدد و بیشتر، رسیده باشد که این به معنی زحمت بیشتر و تغییرات پر هزینه‌تر است.

- احتمال کند شدن ماشین سرویس دهنده در اثر اجرای تعداد زیادی SP ؛ چناچه بخش زیادی از منطق برنامه از طریق SP اجرا شود، سرور دیتابیس موظف به اجرای آنهاست. اما در صورتیکه منطق، در کد برنامه قرار داشته باشد، امکان توزیع آن بر روی سرور‌های مجزا و یا حتی ماشین کلاینت وجود خواهد داشت. امروزه اکثر کلاینت‌ها به دیتابیس‌های سبک و سریعی مجهز شده‌اند. بنابراین در صورت امکان چرا بار پردازشی را به عهده آنها نگذاریم؟! 

- یکپارچگی کمتر؛ تقریبا همه اپلیکیشن‌ها نیازمند ارتباط با سایر سیستم‌ها هستند. اگر بخش‌های زیادی از منطق برنامه درون SP مخفی شده باشند، این نقطه تلاقی بین سیستمی، احتمالا درون خود دیتابیس قرار میگیرد و این به معنی ایجاد SP های بیشتر، افزودن پارامتر‌های بیشتر، توسعه SPهای قبلی و بطور خلاصه اعمال تغییرات بیشتر، که منتج به قابلیت نگهداری کمترخواهد شد.

- انعطاف پذیری کمتر؛ در یک شرایط ایده آل، عملکرد اپلیکیشن، مستقل از دیتابیس است. اگر نیاز به تغییر دیتابیس، مثلا از اوراکل به Microsoft SQL Server وجود داشته باشد، نیاز به بازنویسی و انتقال فانکشن‌ها و SP ها محتمل است و از آنجائیکه که با وجود استانداردها، دیتابیس‌های مختلف، معمولا در Syntax دستورات، تفاوت‌های فاحشی دارند، هر چه کد بیشتری در SP ها باشد، نیاز به انتقال و تبدیل بیشتری وجود دارد. 

- عدم وجود بازخورد مناسب؛ بسیاری از اوقات در صورت بروز اشکالی در حین اجرای یک SP، فقط با یک متن ساده بصورت Table has no rows   و یا  error مواجه میشویم. چنین خطاهایی هنگام دیباگ اصلا خوشایند نیستند. MS SQL در این بین بازخورد‌های مناسبی را ارائه میکند. اگر تجربه کار با سایر دیتابیس‌ها را داشته باشید، اهمیت بازخورد‌های مناسب، ملموس‌تر خواهد بود.

- کد نویسی سخت‌تر؛ نوشتن کد SQL  معمولا در همان IDE  اپلیکیشن انجام نمیشود. جابجایی مداوم بین دو IDE ، دیباگ و کد نویسی از طریق دو اینترفیس مجزا، اصلا ایده‌ال نیست. 

- SP  منطق را بیش از حد پنهان میکند؛ حتی با دانستن نام صحیح یک SP، باز هم تصویری از پارامتر‌های ارسالی به آن و نتیجه برگشتی نخواهیم داشت. نمیدانیم نتیجه حاصل از اجرای SP ما مقداری را برمیگرداند یا خیر؟ در صورت وجود برگشتی، یک Cursor است یا یک مقدار؟ اگر Cursor است شامل چه ستون‌هایی است؟

- SP نمیتواند یک شیء را به عنوان آرگومان بپذیرد؛ بنابراین احتمال کثیف شدن کد به مرور افزایش پیدا میکند و بدتراز آن، در صورت ارسال اشتباه یک پارامتر، یا عدم  تطابق تعداد پارامتر‌ها، مجبور به بررسی تمام آنها بصورت دستی هستیم. برای مثال دو قطعه کد زیر را با هم مقایسه کنید:

INSERT INTO User_Table(Id,Username,Password,FirstName,SureName,PhoneNumber,x,Email)
VALUES (1,'VahidN','123456','Vahid','Nasiri','09120000000','vahid_xxx@example.com')

و معادل آن در یک ORM  فرضی:

public void Insert(User user)
{
  _users.Insert(user);
  db.Save();
}

به‌وضوح قطعه کد sql، قبل از خوب یا بد بودن، زشت است. همچنین پارامتر x آن که فرضاً به تازگی اضافه شده، مقداری را دریافت نکرده و باعث بروز خطا خواهد شد.

- نبود Query Chaining؛ یکی از ویژگی‌های جذاب ORM‌‌های امروزی، امکان تشکیل یک کوئری با قابلیت خوانایی بالا و افزودن شرط‌های بیشتر از طریق  الگوی builder است. قطعه کد زیر یک SP برای جستجوی داینامیک نام و نام خانوادگی در یک جدول فرضی به اسم Users است:

public ICollection<User> GetUsers(string firstName,string lastName,Func<User, bool> orderBy)
{
    var query = _users.where(u => u.LastName.StartsWith(lastName));
    query = query.where(u => u.FirstName.StartsWith(firstName));
    query = query.OrderBy(orderBy);
    return  query.ToList();
}

در مقایسه با معادل SP آن:

CREATE PROCEDURE DynamicWhere 
    @LastName varchar(50) = null,
    @FirstName varchar(50) = null,
    @Orderby varchar(50) = null
AS
BEGIN
    DECLARE @where nvarchar(max)
    SELECT @where = '1 = 1'
 
    IF @LastName IS NOT NULL
        SELECT @Where = @Where + " AND A.LastName LIKE @LastName + '%'"
 
    IF @FirstName IS NOT NULL
        SELECT @Where = @Where + " AND A.FirstName LIKE @FirstName + '%'"
 
    DECLARE @orderBySql nvarchar(max)
    SELECT @orderBySql = CASE
        WHEN @OrderBy = "LastName" THEN "A.LastName"
        ELSE @OrderBy = "FirstName" THEN "A.FirstName"
    END
 
    DECLARE @sql nvarchar(max)
    SELECT @sql = "
    SELECT A.Id , A.AccountNoId, A.LastName, A.FirstName, A.PostingDt, 
    A.BillingAmount
    FROM Users 
    WHERE " + @where + " 
    ORDER BY " + @orderBySql
 
    exec sp_executesql @sql,  N'@LastName varchar(50), @FirstName varchar(50)
        @LastName, @FirstName
END

حاجت به گفتن نیست که قطعه کد اول چقدر خواناتر، انعطاف پذیرتر، خلاصه‌تر و قابل نگهداری‌تر است.

- نداشتن امکانات زبان‌های مدرن؛ زبان‌ها و IDE‌های مدرن، امکانات قابل توجهی را برای نگهداری بهتر، انعطاف پذیری بیشتر، مقیاس پذیری بالاتر، تست پذیری دقیق‌تر و... ارائه میکنند. به عنوان مثال:

  • شیءگرایی و امکانات آن که در SP موجود نیست و در مورد قبلی معایب، به آن مختصرا اشاره شد. در نظر بگیرید اگر SQL زبانی شیء گرا بود و مجهز به ارث بری و کپسوله سازی بود، چقدر قابلیت نگهداری آن بالاتر میرفت و حجم کد‌های نوشته شده میتوانست کمتر باشند.
  • نداشتن Lazy Loading که باعث مصرف زیاد حافظه میشود.
  • نداشتن intellisense حین فراخوانی‌ها.
  • نداشتن Navigation Property که باعث join نویسی‌های زیاد خواهد شد.
  • SQL در مقایسه با یک زبان مدرن ناقص بنظر میرسد و این نوشتن کد آن را سخت‌تر میکند.‌
  • نداشتن امکان تغییر منطقی نام جداول و ستون ها
  • مدیریت تراکنش‌ها بصورت دستی، حال آنکه با الگوی Unit Of Work  این مشکل در یک ORM قدرتمند مثل EF حل شده است.


- زمان بر بودن نوشتن SP؛ گاهی نوشتن یک تابع در یک ORM یا بعضا نوشتن یک کوئری SQL کوتاه در یک رشته متنی، ساده‌تر از نوشتن کد SP است. آیا برای هر وظیفه کوچک در دیتابیس، نوشتن یک SP ضروری است؟


مزایای SP :

- کمتر کردن Round Trips در شبکه و متعاقبا کاهش ترافیک شبکه؛ اگر از یک فراخوانی استفاده کنیم، کاهش Round Trip‌ها تاثیر چندانی نخواهد داشت. همچنین ارسال یک کوئری کامل، نسبت به ارسال فقط اسم SP و پارامتر‌های آن، پهنای باند بیشتری اِشغال میکند. البته در یک شبکه با سرعت قابل قبول، بعید است این دو مزیت محسوس باشند؛ اما به هر حال برای موارد خاص، دو مزیت محسوب میشوند. نکته دیگر آنکه بدلیل Pre-Compiled بودن SP‌ها و همچنین کَش شدن Execution Plan آنها، اندکی با سرعت بالاتری اجرا میشوند.

- امکان چک کردن سینتکس قبل از اجرای آن؛ در مقایسه با Raw Query مزیت محسوب میشود.

امکان به اشتراک گذاری کد؛ برای پروژه‌هایی که چندین اپلیکیشن با چندین زبان برنامه نویسی مختلف در حال تهیه هستند و نیازمند دسترسی مستقیم به داده‌ها با سرعت به نسبت بالاتری هستند، SP  میتواند یک راه حل ایده آل محسوب شود. بجای پیاده سازی منطق برنامه در هر اپلیکیشن بصورت جداگانه و زحمت کدنویسی هرکدام، میتوان از SP  استفاده کرد. هرچند امروزه معمولا برای حل این مشکل، API های مشترک معماری Restful  ارجحیت دارد. 

- کمک به ایجاد یک پَک؛ در یک زیر سیستم با نیازمندی مشخص که اعمال تغییرات در آن محتمل نمیباشد نیز SP میتواند یک گزینه مناسب به حساب آید. مثلا یک سیستم Membership را در نظر بگیرید که در پروژه‌های مختلف شما مورد استفاده قرار خواهد گرفت. برای مثال میشود یک سیستم Membership  سفارشی را با امکان  Hash  پسورد و  رمز کردن داده‌های حساس،  به کمک SP و Function ‌های مناسب فراهم کرد و در واقع بین Application Login  و Data Logic تمایز قائل شد. شخصا معماری Restful را به این روش هم ترجیح میدهم. 

بهرمند شدن از امکانات بومی SQL ؛ به عنوان نمونه برای ترانهاده کردن خروجی یک کوئری میتوان از فانکشن  Pivot  استفاده کرد. یا فانکشن‌های تحلیلی  Lead  و  Lag  (لینک مستندات اوراکل این دو فانکشن به ترتیب در ^ و ^ ) که بنظر نمیرسد هنوز معادل مستقیمی درORM  ها  داشته باشند. 

تسلط و کنترل بیشتر و دقیقتر بر کوئری نهایی؛ گفته میشود SP و عبارات SQL در دیتابیس، حکم assembly را در سایر زبان‌ها دارند. بنابراین با SP میتوان عبارات SQL و نحوه اجرای آن را در دیتابیس، بطور کامل تحت فرمان داشت. این در حالی است که هر یک از ORM‌ها دستورات زبان برنامه نویسی مبداء را به یک عبارت SQL ترجمه میکنند که این عبارت چندان تحت کنترل برنامه نویس نیست و بیشتر به مدل کاری ORM بستگی دارد. 

امکان join بین دو یا چند دیتابیس مجزا؛ حال آنکه امکان join بین دو Context در ORM ‌ها وجود ندارد. بعلاوه اگر دو دیتابیس مدنظر ما روی دو سرور مجزا باشند، با SP و  کانفیگ Linked Server  کماکان میشود کوئری join  دار نوشت.

برای عملیات‌های Batch مناسب‌تر است؛ در مقام مقایسه با ORM ‌ها که با تکنیک‌های مختلفی سعی در افزایش سرعت عملیات Batch، بخصوص Insert و Update را دارند، SP  با سرعت قابل قبول‌تری اجرا میشود.

عدم نیاز به یادگیری سینتکس و ابزاری جدید؛ موارد بسیاری وجود دارند که فرصت یادگیری تکنولوژی جدیدی مثل یک ORM و یا SQL Bulk و حتی کتابخانه‌های ثالث مبتنی بر این ابزارها  وجود ندارند و ممکن است مجبور شوید برای باقی ماندن در بازار رقابتی، از دانسته‌های قبلی خود استفاده کنید .

تخصصی‌تر کردن وظایف؛ برنامه نویس‌های دیتابیس به صورت تخصصی اقدام به تحلیل روابط و ایندکس‌ها میکنند، دیتابیس را ایجاد و نرمال سازی مینمایند، SP های متناسب را میسازند و به بهترین شکل Optimize و در آخر تست میکنند.

- امنیت به نسبت بالاتر؛ میتوان مجوز اجرای SP را به یک کاربر اعطا کرد، بدون آنکه مجوز دسترسی به جداول مورد استفاده در آن SP را داد. همچنین نسبت به کوئری‌های پارامتری نشده، SQL ارجیحت دارند چون احتمال آسیب پذیری در مقابل SQL Injection را کمتر میکنند.


نتیجه‌گیری

اگرچه SP ها برای پردازش داده‌ها آنقدر هم که در وبلاگ‌ها میخوانیم بد نیستند، اما سوء استفاده از آن، مشکلات عدیده‌ای را ایجاد خواهد کرد. با توجه به روند تغییرات تکنولوژی‌های دسترسی به داده‌ها و معماری‌های مدرن بنظر میرسد SP در بهترین حالت، ابزار مناسبی برای انجام عملیات CRUD است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.

مطالب
معرفی Selector های CSS - قسمت 7
61- :any-link
تمامی تگ‌هایی را انتخاب می‌کند که می‌توانند نقش لینک را در صفحه ایفا کنند. در واقع تگ‌های a، area و link را انتخاب می‌نماید که دارای ویژگی href هستند و به عنوان لینک عمل می‌کنند.
<style>
    :any-link {
        color: red;
    }
</style>
<a href="page1.htm">Link 1</a>
در مثال فوق، متن Link 1 به رنگ قرمز نمایش می‌یابد.
           Selector  نسخه CSS
No No
No
No No :any-link  4
62- :local-link
تمامی لینک هایی را انتخاب می‌کند که با آدرس صفحه‌ی جاری یکسان می‌باشند.
<style>
    :local-link {
        color: red;
    }
</style>
<a href="http://www.example.com/article/">Link 1</a>
<a href="http://www.example.com/article">Link 2</a>
<a href="http://www.example.com/article/">Link 3</a>
<a href="http://www.example.com/article/12">Link 4</a>
با فرض اینکه آدرس صفحه‌ی جاری http://www.example.com/article/   می باشد مثال فوق را بررسی می‌کنیم. متن Link 1 و Link3 به رنگ قرمز نمایش می‌یابند، زیرا مشابه آدرس صفحه‌ی جاری می‌باشند.
           Selector  نسخه CSS
No No
No
No No :local-link  4 
63- :local-link(n)
تمامی لینک هایی را انتخاب می‌کند که آدرس آنها با آدرس صفحه‌ی جاری یکسان بوده و n بخش از آدرس آنها با بخش‌های آدرس جاری یکسان باشند. در واقع n، تعداد بخش هایی از آدرس لینک را مشخص می‌کند که با بخش‌های آدرس صفحه‌ی جاری مطابقت داشته باشند. در مطابقت آدرس، از scheme، username، password، port، query string و fragment صرف نظر می‌شود تا یکسان بودن آدرس را تشخیص دهد. سپس تشخیص می‌دهد کدام بخش‌های آدرس با هم یکسان می‌باشند. جهت اطلاع باید عرض کنم که شکل کامل یک آدرس به صورت scheme:[//[user:password@]host[:port]][/]path[?querystring][#fragment] می‌باشد.
<style>
    :local-link {
        color: red;
    }
    :local-link(0) {
        text-decoration: none;
    }
    :local-link(1) {
        text-decoration: overline;
    }
    :local-link(2) {
        background: blue;
    }
</style>
<a href="http://www.example.com">Link 1</a>
<a href="http://www.example.com/2016">Link 2</a>
<a href="http://www.example.com/2016/01">Link 3</a>
<a href="http://www.example.com/2016/01/">Link 4</a>
<a href="http://www.example.com/2016/01/02">Link 5</a>
<a href="https://www.example.com/2016/01/">Link 6</a>
<a href="http://example.com/2016/01">Link 7</a>
با فرض اینکه آدرس صفحه‌ی جاری http://www.example.com/2016/01/ می باشد مثال فوق را بررسی میکنیم. Link 1 تحت تاثیر Selector دوم، بدون زیرخط نمایش می‌یابد. Link 2 تحت تاثیر Selector دوم و سوم، بدون زیرخط و با روخط نمایش می‌یابد. Link 3، Link 5 و Link 6 تحت تاثیر Selector دوم و سوم و چهارم، بدون زیرخط، با روخط و پس زمینه‌ی آبی نمایش می‌یابند. Link 6 به دلیل استفاده از https نمی‌تواند تحت تاثیر Selector اول قرار بگیرد. Link 4 تحت تاثیر Selector اول و دوم و سوم و چهارم، به رنگ قرمز، بدون زیرخط، با روخط و پس زمینه‌ی آبی نمایش می‌یابد. Link 7 هم بدون تاثیر هیچ Selector ی بر روی آن، بدون قالب باقی می‌ماند، زیرا قسمت host لینک با آدرس مطابقت ندارد.
           Selector  نسخه CSS
No No
No
No No :local-link(n)  4
64- :active-drop-target
تگی را انتخاب می‌کند که در حال حاضر به عنوان مقصد یک تگ drag (کشیده) شده در نظر گرفته شده است. به عبارت دیگر، تگی را با عمل drag کشیده ایم و بر روی یک تگ مقصد قرار داده ایم، ولی هنوز عمل drop یا رها کردن صورت نگرفته است.
              Selector  نسخه CSS
No No
No
No No :active-drop-target   4 
65- :valid-drop-target
تگی را انتخاب می‌کند که در حال حاضر به عنوان مقصد یک تگ drag (کشیده) شده در نظر گرفته شده است و برای عمل drop معتبر می‌باشد. به عبارت دیگر، تگ مقصد امکان پذیرش یک تگ را با عمل Drag & Drop دارد.
              Selector  نسخه CSS
No No
No
No No :valid-drop-target   4 
66- :invalid-drop-target
تگی را انتخاب می‌کند که در حال حاضر به عنوان مقصد یک تگ drag (کشیده) شده در نظر گرفته شده است و برای عمل drop معتبر نمی‌باشد. به عبارت دیگر، تگ مقصد امکان پذیرش یک تگ را با عمل Drag & Drop ندارد.
              Selector  نسخه CSS
No No
No
No No :invalid-drop-target   4 
<style>
    :active-drop-target {
        background: blue;
    }
    :valid-drop-target {
        border: 1px solid green;
    }
    :invalid-drop-target {
        border: 1px solid red;
    }
    .container {
        width: 200px;
        height: 200px;
    }
</style>
<div class="container"></div>
<img src="image.jpg" draggable="true"/>
در مثال فوق، با Drag نمودن تصویر بر روی تگ div، تحت تاثیر Selector اول، رنگ پس زمینه‌ی div آبی می‌شود. اگر امکان پذیرش img توسط تگ div وجود داشته باشد، کادری سبز رنگ دور تگ div نمایش می‌یابد در غیر اینصورت کادر قرمز رنگ دور آن نمایش خواهد یافت.
67- :current
تگی را انتخاب می‌کند که در حال حاضر در حال نمایش یا ارائه می‌باشد. این Selector در زمان پردازش صوت، تصویر، نمایش زیر نویس و غیره در تگ canvas مورد استفاده قرار می‌گیرد.
              Selector  نسخه CSS
No No
No
No No :current   4 
68- :past
تگی را انتخاب می‌کند که قبل از :current نمایش یافته است.
              Selector  نسخه CSS
No No
No
No No :past   4 
69- :future
تگی را انتخاب می‌کند که بعد از :current نمایش یافته است.
              Selector  نسخه CSS
No No
No
No No :future   4 
هر سه Selector فوق می‌توانند با دریافت آرگومان به صورت مجموعه ای از Selector ها، تگ‌های خاصی را انتخاب کنند یا نتیجه‌ی انتخاب را محدود نمایند. اگر بدون آرگومان به کار روند، تک مورد نظر را بدون در نظر گرفتن محدویت انتخاب می‌نمایند.
<style>
    :past(div,p) {
        background: red;
    }
    :current(div,p) {
        background: green;
    }
    :future(div,p) {
        background: yellow;
    }
</style>
در مثال فوق، تگ‌های div یا p که در حال حاضر در حال نمایش می‌باشند، با پس زمینه‌ی سبز، تگی که قبل از :current نمایش یافته است با پس زمینه‌ی قرمز و تگی که بعد از :current نمایش خواهد یافت با پس زمینه‌ی زرد نمایش می‌یابد.
70- :placeholder-shown
تمامی تگ‌های input و textarea را انتخاب می‌کند که در حال نمایش یا حاوی placeholder می‌باشند و هنوز متنی در آنها وارد نشده است. در حال حاضر به صورت آزمایشی و با vendor prefix قابل استفاده است.
<style>
    :placeholder-shown {
        color: gray;
    }
</style>
<input placeholder="Enter Username"/>
<input placeholder="Enter Password"/>
در مثال فوق، اگر در هر کدام از تگ‌های input، هیچ متنی وارد نشده باشد، متن‌های Enter Username و Enter Password به رنگ طوسی نمایش می‌یابند.
              Selector  نسخه CSS
-webkit- -wekit-
-ms-
-moz- -webkit- :placeholder-shown   4 















مطالب
قرار دادن نمودارهای MS Chart در گزارشات PdfReport
در حالت کلی، هر شیءایی را که بتوان تبدیل به تصویر کرد، قابلیت قرارگیری در یک فایل PDF را هم خواهد داشت. از این نمونه می‌توان به اشیاء MSChart اشاره کرد که از دات نت 4 جزئی از کتابخانه‌های اصلی دات نت شده‌اند و البته برای دات نت سه و نیم نیز به صورت جداگانه قابل دریافت است.
در ادامه مثالی را بررسی خواهیم کرد که بر اساس ردیف‌های گزارش آن، یک نمودار، به انتهای گزارش اضافه خواهد شد.
کدهای کامل این مثال را در ذیل مشاهده می‌کنید:
using System;
using System.Collections.Generic;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.ChartImage
{
    public class ChartImagePdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            var chart = new MSChartHelper
                {
                    AxisXTitle = "User",
                    AxisYTitle = "Balance",
                    ChartTitle = "Users Balance",
                    AxisTitleFont = new System.Drawing.Font("Tahoma", 12f),
                    LabelStyleFont = new System.Drawing.Font("Tahoma", 10f),
                    ChartTitleFont = new System.Drawing.Font("Arial", 16f, System.Drawing.FontStyle.Bold),
                    LegendsFont = new System.Drawing.Font("Tahoma", 12f)
                };

            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(string.Format("{0}\\fonts\\irsans.ttf", AppPath.ApplicationPath),
                            string.Format("{0}\\fonts\\verdana.ttf", Environment.GetEnvironmentVariable("SystemRoot")));
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.ClassicTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.Relative);
             })
             .MainTableDataSource(dataSource =>
             {
                 var listOfRows = new List<User>();
                 for (var i = 0; i < 7; i++)
                 {
                     listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = (i * 50) + 1000 });
                 }
                 dataSource.StronglyTypedList(listOfRows);
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
                 events.DocumentOpened(args =>
                 {
                     chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250);
                 });
                 events.RowAdded(args =>
                 {
                     if (args.RowType == RowType.DataTableRow)
                     {
                         var name = args.TableRowData.GetValueOf<User>(x => x.Name);
                         if (name == null) return;

                         var balance = args.TableRowData.GetValueOf<User>(x => x.Balance);
                         if (balance == null) return;

                         chart.AddXY(name, balance);
                     }
                 });
                 events.DocumentClosing(args =>
                 {
                     chart.AddChartToPage(args.PdfDoc);
                     chart.FreeResources();
                 });
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.Width(1);
                     column.HeaderCell("ردیف", captionRotation: 90);
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Id);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.Width(2);
                     column.HeaderCell("شماره");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Name);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(2);
                     column.Width(2);
                     column.HeaderCell("نام");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.LastName);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     column.Width(3);
                     column.HeaderCell("نام خانوادگی");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Balance);
                     column.HeaderCell("موجودی");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.Width(2);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                 });
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptChartSample.pdf"));
        }
    }
}
برای تهیه این گزارش و افزودن نمودار به آن، از کلاس کمکی ذیل استفاده شده است:
using System.Drawing;
using System.IO;
//It's part of the .NET 4.0+ now.
using System.Windows.Forms.DataVisualization.Charting;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.ChartImage
{
    public class MSChartHelper
    {
        // MS Chart learning tutorials:
        // http://weblogs.asp.net/scottgu/archive/2010/02/07/built-in-charting-controls-vs-2010-and-net-4-series.aspx
        Chart _chart;

        public System.Drawing.Font AxisTitleFont { set; get; }

        public string AxisXTitle { set; get; }

        public string AxisYTitle { set; get; }

        public string ChartTitle { set; get; }

        public System.Drawing.Font ChartTitleFont { set; get; }

        public System.Drawing.Font LabelStyleFont { set; get; }

        public System.Drawing.Font LegendsFont { set; get; }

        public void AddChartToPage(Document pdfDoc,
                                   int scalePercent = 100,
                                   float spacingBefore = 20,
                                   float spacingAfter = 10,
                                   float widthPercentage = 80)
        {
            using (var chartimage = new MemoryStream())
            {
                _chart.SaveImage(chartimage, ChartImageFormat.Bmp); //BMP gives the best compression result

                var iTextSharpImage = PdfImageHelper.GetITextSharpImageFromByteArray(chartimage.GetBuffer());
                iTextSharpImage.ScalePercent(scalePercent);
                iTextSharpImage.Alignment = Element.ALIGN_CENTER;

                var table = new PdfPTable(1)
                {
                    WidthPercentage = widthPercentage,
                    SpacingBefore = spacingBefore,
                    SpacingAfter = spacingAfter
                };
                table.AddCell(iTextSharpImage);

                pdfDoc.Add(table);
            }
        }

        public void AddXY(object xValue, params object[] yValue)
        {
            _chart.Series[0].Points.AddXY(xValue, yValue);
        }

        public void ChartInit(int width, int height)
        {
            _chart = new Chart
            {
                Width = width,
                Height = height,
                AntiAliasing = AntiAliasingStyles.All,
                TextAntiAliasingQuality = TextAntiAliasingQuality.High,
                Palette = ChartColorPalette.BrightPastel,
                BackColor = ColorTranslator.FromHtml("#F3DFC1"),
                BackGradientStyle = GradientStyle.TopBottom
            };

            setBorder();
            setTitles();
            setChartAreas();
            setLegends();
            setSeries();
        }

        public void FreeResources()
        {
            if (_chart != null && !_chart.IsDisposed)
                _chart.Dispose();
        }        

        private void setBorder()
        {
            _chart.BorderSkin.SkinStyle = BorderSkinStyle.Emboss;
            _chart.BorderlineWidth = 2;
            _chart.BorderlineColor = Color.FromArgb(181, 64, 1);
            _chart.BorderlineDashStyle = ChartDashStyle.Solid;
        }

        private void setChartAreas()
        {
            _chart.ChartAreas.Add("ChartArea1");
            _chart.ChartAreas[0].AxisX.Title = AxisXTitle;
            _chart.ChartAreas[0].AxisY.Title = AxisYTitle;
            _chart.ChartAreas[0].AxisX.TitleFont = AxisTitleFont;
            _chart.ChartAreas[0].AxisY.TitleFont = AxisTitleFont;
            _chart.ChartAreas[0].AxisX.LabelStyle.Font = LabelStyleFont;
            _chart.ChartAreas[0].AxisX.LabelStyle.Angle = -90;
            _chart.ChartAreas[0].BackColor = Color.White;
            _chart.ChartAreas[0].AxisX.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisY.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.FromArgb(64, 64, 64);
        }

        private void setLegends()
        {
            _chart.Legends.Add("Default");
            _chart.Legends[0].LegendStyle = LegendStyle.Row;
            _chart.Legends[0].IsTextAutoFit = false;
            _chart.Legends[0].DockedToChartArea = "ChartArea1";
            _chart.Legends[0].Docking = Docking.Bottom;
            _chart.Legends[0].IsDockedInsideChartArea = false;
            _chart.Legends[0].BackColor = Color.Transparent;
            _chart.Legends[0].Font = LegendsFont;
        }

        private void setSeries()
        {
            _chart.Series.Add("");
            _chart.Series[0].ChartType = SeriesChartType.Column;
            _chart.Series[0].Palette = ChartColorPalette.EarthTones;
            _chart.Series[0].IsValueShownAsLabel = true;
            _chart.Series[0].IsVisibleInLegend = false;
        }

        private void setTitles()
        {
            _chart.Titles.Add(ChartTitle);
            _chart.Titles[0].Font = ChartTitleFont;
            _chart.Titles[0].TextStyle = TextStyle.Shadow;
            _chart.Titles[0].ShadowOffset = 3;
            _chart.Titles[0].ShadowColor = Color.FromArgb(32, 0, 0);
            _chart.Titles[0].Alignment = ContentAlignment.TopCenter;
            _chart.Titles[0].ForeColor = Color.FromArgb(26, 59, 105);
        }
    }
}

توضیحات:
- استفاده از MSChart در اینجا از این جهت مناسب است که فراگیری کار کردن با آن عمومی بوده و در پروژه‌های وب و ویندوز کاربرد دارد و احتمالا هم اکنون با نحوه کارکردن با آن آشنا هستید، زیرا از سال 2010 به دات نت اضافه شده است.
- در این بین تنها متد جدید و مهم کلاس MSChartHelper، متد AddChartToPage آن است. به کمک متد chart.SaveImage می‌توان تصویر نهایی معادل یک نمودار را در حافظه ذخیره کرد. سپس با استفاده از متد PdfImageHelper.GetITextSharpImageFromByteArray، این تصویر موجود در حافظه را به معادل قابل استفاده آن در iTextSharp تبدیل کرده و به صفحه اضافه خواهیم کرد.
- در کدهای اصلی تولید گزارش، مقدار دهی کلاس کمکی MSChartHelper در قسمت رخدادهای قابل استفاده PdfReport در متد MainTableEvents آن انجام شده است:
در روال رویدادگردان DocumentOpened، بر اساس عرض واقعی صفحه، عرض نمودار را مشخص می‌کنیم:
                 events.DocumentOpened(args =>
                 {
                     chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250);

                 });
سپس در روال رویدادگردان RowAdded، فرصت خواهیم داشت به اطلاعات در حال افزوده شدن به گزارش دسترسی داشته باشیم. این اطلاعات را به متد افزودن XY نمودار ارسال خواهیم کرد:
                 events.RowAdded(args =>
                 {
                     if (args.RowType == RowType.DataTableRow)
                     {
                         var name = args.TableRowData.GetValueOf<User>(x => x.Name);
                         if (name == null) return;

                         var balance = args.TableRowData.GetValueOf<User>(x => x.Balance);
                         if (balance == null) return;

                         chart.AddXY(name, balance);
                     }
                 });
و در آخر، پیش از بسته شدن فایل PDF تولیدی (DocumentClosing)، نمودار نهایی را به صفحه اضافه کرده و منابع مرتبط با آن‌را آزاد خواهیم کرد:
                 events.DocumentClosing(args =>
                 {
                     chart.AddChartToPage(args.PdfDoc);
                     chart.FreeResources();
                 });
بنابراین اگر این سؤال عمومی وجود دارد که آیا می‌توان در این بین، به ابتدا و انتهای گزارش اشیایی را افزود، روش کلی آن‌را در روال‌های فوق ملاحظه می‌کنید. توسط args.PdfDoc و args.PdfWriter می‌توان به زیرساخت iTextSharp دسترسی یافت.
 

مطالب دوره‌ها
تزریق خودکار وابستگی‌ها در برنامه‌های ASP.NET Web forms
همانطور که در قسمت‌های قبل عنوان شد، دو نوع متداول تزریق وابستگی‌ها وجود دارند:
الف) تزریق وابستگی‌ها در سازنده کلاس
ب) تزریق وابستگی‌ها در خواص عمومی کلاس‌ها یا Setters injection

حالت الف متداول‌ترین است و بیشتر زمانی کاربرد دارد که کار وهله سازی یک کلاس را می‌توان راسا انجام داد. اما در فرم‌ها یا یوزرکنترل‌های ASP.NET Web forms به صورت پیش فرض کار وهله سازی فرم‌ها و یوزرکنترل‌ها توسط موتور ASP.NET انجام می‌شود و در این حالت اگر بخواهیم از تزریق وابستگی‌ها استفاده کنیم، مدام به همان روش معروف Service locator و استفاده از container.Resolve در تمام قسمت‌های برنامه می‌رسیم که آنچنان روش مطلوبی نیست.
اما ... در ASP.NET Web forms می‌توان وهله سازی فرم‌ها را نیز تحت کنترل قرار داد، که برای آن دو روش زیر وجود دارند:
الف) یک کلاس مشتق شده را از کلاس پایه PageHandlerFactory تهیه کنیم. این کلاس را پیاده سازی کرده و نهایتا بجای وهله ساز پیش فرض فرم‌های موتور داخلی ASP.NET، در فایل وب کانفیگ برنامه استفاده کنیم. یک نمونه از پیاده سازی آن‌را در اینجا می‌توانید مشاهده کنید.
مشکلی که این روش دارد سازگاری آن با حالت Full trust است. یعنی برنامه شما در یک هاست Medium trust (اغلب هاست‌های خوب) اجرا نخواهد شد.
ب) روش دوم، استفاده از یک Http Module است برای اعمال Setter injectionها، به صورت خودکار. اکنون که حالت الف را همه جا نمی‌توان بکار برد یا به عبارتی نمی‌توان وهله سازی فرم‌ها را راسا در دست گرفت، حداقل می‌توان خواص عمومی اشیاء صفحه تولید شده را مقدار دهی کرد که در ادامه، این روش را بررسی می‌کنیم.


تهیه ماژول انجام Setters injection به صورت خودکار در برنامه‌های ASP.NET Web forms به کمک StructureMap

پیشنیاز این بحث، مطلب «استفاده از StructureMap به عنوان یک IoC Container» می‌باشد که پیشتر مطالعه کردید (در حد نحوه نصب StructureMap و آشنایی با تنظیمات اولیه آن)
using System.Collections;
using System.Web;
using System.Web.UI;
using StructureMap;

namespace DI05.Core
{
    /// <summary>
    /// تسهیل در کار تزریق خودکار وابستگی‌ها در سطح فرم‌ها و یوزرکنترل‌ها
    /// </summary>
    public class StructureMapModule : IHttpModule
    {
        public void Dispose()
        { }

        public void Init(HttpApplication app)
        {
            app.PreRequestHandlerExecute += (sender, e) =>
            {
                var page = HttpContext.Current.Handler as Page; // The Page handler
                if (page == null)
                    return;

                WireUpThePage(page);
                WireUpAllUserControls(page);
            };
        }

        private static void WireUpAllUserControls(Page page)
        {
            // در اینجا هم کار سیم کشی یوزر کنترل‌ها انجام می‌شود
            page.InitComplete += (initSender, evt) =>
            {
                var thisPage = (Page)initSender;
                foreach (Control ctrl in getControlTree(thisPage))
                {
                    // فقط یوزر کنترل‌ها بررسی شدند
                    // اگر نیاز است سایر کنترل‌های قرار گرفته روی فرم هم بررسی شوند شرط را حذف کنید
                    if (ctrl is UserControl)
                    {
                        ObjectFactory.BuildUp(ctrl);
                    }
                }
            };
        }

        private static void WireUpThePage(Page page)
        {
            ObjectFactory.BuildUp(page); // برقراری خودکار سیم کشی‌ها در سطح صفحات
        }

        private static IEnumerable getControlTree(Control root)
        {
            foreach (Control child in root.Controls)
            {
                yield return child;
                foreach (Control ctrl in getControlTree(child))
                {
                    yield return ctrl;
                }
            }
        }
    }
}
در این ماژول، کار با HttpContext.Current.Handler شروع می‌شود که دقیقا معادل با وهله‌ای از یک صفحه یا فرم می‌باشد. اکنون که این وهله را داریم، فقط کافی است متد ObjectFactory.BuildUp مربوط به StructureMap را روی آن فراخوانی کنیم تا کار Setter injection را انجام دهد. مرحله بعد یافتن یوزر کنترل‌های احتمالی قرار گرفته بر روی صفحه و همچنین فراخوانی متد ObjectFactory.BuildUp، بر روی آن‌ها می‌باشد.
پس از تهیه ماژول فوق، باید آن‌را در فایل وب کانفیگ برنامه معرفی کرد:
<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />

    <httpModules>
      <add name="StructureMapModule" type="DI05.Core.StructureMapModule"/>
    </httpModules>
  </system.web>

  <system.webServer>    
    <modules runAllManagedModulesForAllRequests="true">
      <add name="StructureMapModule" type="DI05.Core.StructureMapModule"/>
    </modules>
    <validation validateIntegratedModeConfiguration="false" />
  </system.webServer>
</configuration>

مثالی از نحوه استفاده از StructureMapModule تهیه شده

فرض کنید لایه سرویس برنامه دارای اینترفیس‌ها و کلاس‌های زیر است:
namespace DI05.Services
{
    public interface IUsersService
    {
        string GetUserEmail(int id);
    }
}


namespace DI05.Services
{
    public class UsersService: IUsersService
    {
        public string GetUserEmail(int id)
        {
            //فقط جهت بررسی تزریق وابستگی‌ها
            return "test@test.com";
        }
    }
}
کار تنظیمات اولیه آن‌ها را در فایل global.asax.cs برنامه انجام خواهیم داد:
using System;
using StructureMap;
using DI05.Services;

namespace DI05
{
    public class Global : System.Web.HttpApplication
    {
        private static void initStructureMap()
        {
            ObjectFactory.Initialize(x =>
            {
                x.For<IUsersService>().Use<UsersService>();

                x.SetAllProperties(y =>
                {
                    y.OfType<IUsersService>();
                });
            });
        }

        protected void Application_Start(object sender, EventArgs e)
        {
            initStructureMap();
        }

        void Application_EndRequest(object sender, EventArgs e)
        {
            ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
        }
    }
}
در اینجا فقط باید دقت داشت که ذکر SetAllProperties الزامی است. از این جهت که از روش Setter injection در حال استفاده هستیم.
مرحله آخر هم استفاده از سرویس‌های برنامه به شکل زیر است:
using System;
using DI05.Services;

namespace DI05
{
    public partial class Default : System.Web.UI.Page
    {
        public IUsersService UsersService { set; get; }

        protected void Page_Load(object sender, EventArgs e)
        {
            lblEmail1.Text = string.Format("From Default Page: {0}", UsersService.GetUserEmail(1));
        }
    }
}
همانطور که ملاحظه می‌کنید در این فرم، هیچ خبری از وجود IoC Container مورد استفاده نیست و کار وهله سازی و مقدار دهی سرویس مورد استفاده به صورت خودکار توسط Http Module تهیه شده انجام می‌شود.


دریافت مثال کامل قسمت جاری
DI05.zip


یک نکته‌ی تکمیلی
برای ارتقاء نکات مطلب جاری به نگارش سوم StructureMap نیاز است موارد ذیل را لحاظ کنید:
الف) نصب بسته‌ی وب آن
PM> Install-Package structuremap.web
ب) ReleaseAndDisposeAllHttpScopedObjects حذف شده را به متد جدید ()HttpContextLifecycle.DisposeAndClearAll تغییر دهید.
ج) x.SetAllProperties را به x.Policies.SetAllProperties ویرایش کنید.
مطالب
آزمایش Web APIs توسط Postman - قسمت دوم - ایجاد گردش کاری بین درخواست‌ها
تا اینجا مثال‌هایی را که بررسی کردیم، متکی به خود بودند و اطلاعات هر کدام، از یک درخواست به درخواستی دیگر کاملا متفاوت بود. همچنین اطلاعات ارسالی و یا دریافتی توسط آن‌ها نیز ثابت و از پیش تعیین شده بود.


کار با اطلاعات متغیر دریافتی از سرور

در Postman یک برگه‌ی جدید را باز کنید و سپس آدرس https://httpbin.org/uuid را در حالت Get درخواست نمائید:


این درخواست، یک Guid اتفاقی جدید را باز می‌گرداند. هربار که بر روی دکمه‌ی Send کلیک کنیم، مقدار Guid دریافتی متفاوت خواهد بود. این خروجی دقیقا حالتی است که در برنامه‌های دنیای واقعی با آن سر و کار داریم. در این نوع برنامه‌ها، پیشتر نمی‌توان مقدار خروجی دریافتی از سرور را پیش‌بینی کرد. همچنین علاقمندیم این مقدار دریافتی (از درخواست 1) را به Post request ای که در انتهای قسمت قبل ایجاد کردیم (درخواست 2)، ارسال کنیم و اطلاعاتی را بین درخواست‌ها به اشتراک بگذاریم.
برای این منظور، مجموعه‌ی httpbin ای را که در قسمت قبل ایجاد کردیم، انتخاب کرده و Post request ذخیره شده‌ی در آن‌را انتخاب کنید تا مجددا جزئیات آن بازیابی شود:

اکنون با مراجعه به برگه‌ی بدنه‌ی درخواست آن، قصد داریم یک خاصیت جدید id را با guid دریافتی از سرور توسط درخواستی دیگر، مقدار دهی کنیم:


برای دسترسی به این اطلاعات، می‌توان از ویژگی بسیار قدرتمند postman به نام متغیرها استفاده کرد. به همین جهت به برگه‌ی درخواست دریافت guid مراجعه کرده و برگه‌ی Tests آن‌را باز کنید:


این قسمت پس از پایان درخواست جاری، اجرا می‌شود. بنابراین دراینجا فرصت خواهیم داشت تا مقدار دریافتی از سرور را در یک متغیر ذخیره کنیم. سپس می‌توان از این متغیر در حین ارسال درخواستی دیگر استفاده کرد. در پنل کنار textbox نوشتن آزمون‌ها، تعدادی نمونه کد هم وجود دارند. برای مثال در اینجا نمونه کد «Set a global variable» را با کلیک بر روی آن انتخاب می‌کنیم:
 pm.globals.set("uuid", "foo");
در این مثال key/value این متغیر سراسری، با یک کلید مشخص و مقداری ثابت، مقدار دهی شده‌اند.

اکنون مجددا بر روی دکمه‌ی Send این درخواست کلیک کنید. پس از اجرای آن، این قطعه کد اجرا شده و یک متغیر سراسری، با کلید uuid و مقدار ثابت مشخص شده، در تمام برگه‌های postman در دسترس خواهند بود. برای بررسی صحت این عملیات، می‌توانید بر روی دکمه‌‌ای با آیکن چشم، در بالای سمت راست صفحه، کلیک کنید:


در ادامه برای استفاده‌ی از این متغیر سراسری، به برگه‌ی Post request مراجعه کرده و بدنه‌ی درخواست را به صورت زیر ویرایش می‌کنیم:
{
"name": "Vahid",
"id": "{{uuid}}"
}
در جائیکه بدنه‌ی درخواست درج می‌شود، متغیرها باید درون {{ }} قرار گیرند.
سپس این درخواست را ارسال کنید، در خروجی دریافتی از سرور httpbin، خاصیت و مقدار جدید id قابل مشاهده هستند که به معنای ارسال صحیح آن به سرور است:
    "json": {
        "id": "foo",
        "name": "Vahid"
    },
در اینجا، foo، مقداری است که از یک درخواست دیگر خوانده شده و توسط درخواست مجزای post جاری، ارسال گردیده‌است.
در این مرحله بر روی دکمه‌ی Save برگه‌ی uuid کلیک کرده و آن‌را در مجموعه‌ی httpbin، ذخیره کنید.


روش دسترسی و به اشتراک گذاری مقدار متغیر uuid دریافتی

تا اینجا متغیر سراسری uuid تنظیم شده، دارای یک مقدار ثابت است. برای تنظیم آن به مقدار Guid دریافتی از سرور، مجددا به برگه‌ی Tests درخواست uuid مراجعه می‌کنیم و آن‌را به نحو زیر تکمیل خواهیم کرد:



pm.response حاوی کل اطلاعات شیء response است و برای مثال بدنه‌ی response دریافتی از سمت سرور httpbin، یک چنین شکلی را دارد:
{
    "uuid": "4594e7ad-cae3-487b-bd42-fc49c312c0e9"
}
با فراخوانی متد json بر روی این شیء، اکنون می‌توان به اطلاعات آن همانند یک شیء متداول جاوا اسکریپتی که دارای کلید و خاصیت uuid است، دسترسی یافت:
let jsonResponse = pm.response.json();
pm.globals.set("uuid", jsonResponse.uuid);
پس از این تنظیم، مجددا درخواست را ارسال کنید که سبب دریافت uuid جدیدی می‌شود:
{
    "uuid": "83d437a8-bce6-438b-8693-068e5399182c"
}
برای بررسی نتیجه‌ی این عملیات، با کلیک بر روی دکمه‌‌ای با آیکن چشم، در بالای سمت راست صفحه، چنین خروجی قابل مشاهده خواهد بود:


که به معنای دسترسی به مقدار متغیر uuid دریافتی از سمت سرور و تنظیم آن به عنوان یک متغیر سراسری است.
اکنون اگر مجددا به برگه‌ی مجزای Post request مراجعه و آن‌را ارسال کنیم، در خروجی بدنه‌ی response دریافتی از سمت سرور:
   "json": {
        "id": "83d437a8-bce6-438b-8693-068e5399182c",
        "name": "Vahid"
    },
دقیقا ارسال همان مقدار متغیر دریافتی از برگه‌ای دیگر که توسط متغیر {{uuid}} تنظیم شده‌، قابل مشاهده‌است.
تا اینجا تمام برگه‌های باز را ذخیره کنید:


در این تصویر، uuid پس از Post request قرار گرفته، چون ترتیب ذخیره سازی آن‌ها به همین صورت بوده‌است. اما نیاز است uuid را به پیش از درخواست post، منتقل کرد. برای اینکار می‌توان از drag/drop آیتم آن کمک گرفت.


نوشتن یک آزمون ساده

هدف اصلی از برگه‌ی Tests هر درخواست در Postman، نوشتن آزمون‌های مختلف است. به همین جهت برای نوشتن یک آزمون ساده، به برگه‌ی Post request که هم اکنون باز است، مراجعه کرده و سپس برگه‌ی Tests آن‌را به صورت زیر تنظیم می‌کنیم:


pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});
در اینجا در پنل code snippets کناری آن، بر روی لینک و گزینه‌ی «Status code: code is 200» کلیک کرده‌ایم تا به صورت خودکار، قطعه کد فوق را تولید کند. البته Tests را در قسمت‌های بعدی با جزئیات بیشتری بررسی خواهیم کرد. در اینجا بیشتر هدف آشنایی مقدماتی با برگه‌ی Tests آن است.
این آزمون، بررسی می‌کند که آیا status code بازگشتی از سرور 200 است یا خیر؟ برای اجرای آن، فقط کافی است بر روی دکمه Send این برگه کلیک کنید:


نتیجه‌ی آن‌را در برگه‌ی جدید Test Results، در قسمت نمایش response دریافتی از سمت سرور، می‌توان مشاهده کرد که بیانگر موفقیت آمیز بودن آزمایش انجام شده‌است.
اگر علاقمندید تا حالت شکست آن‌را نیز مشاهده کنید، بجای عدد 200، عدد 404 را وارد کرده و مجددا درخواست را ارسال کنید.


اجرای ترتیبی آیتم‌های یک مجموعه

تا اینجا اگر درخواست‌ها را در مجموعه‌ی httpbin ذخیره کرده و همچنین ترتیب آن‌ها را نیز به نحوی که گفته شد، اصلاح کرده باشید، یک چنین شکلی را باید مشاهده کنید:


در اینجا برای اجرای ترتیبی این آیتم‌ها و اجرای گردش کاری کوچکی که ایجاد کرده‌ایم (درخواست post باید پس از درخواست uuid و بر اساس مقدار دریافتی آن از سرور اجرا شود)، می‌توان از collection runner برنامه‌ی Postman استفاده کرد:


با کلیک بر روی دکمه‌ی Runner، در نوار ابزار برنامه‌ی Postman، صفحه‌ی Collection runner آن ظاهر می‌شود. در این صفحه ابتدا httpbin collection را انتخاب می‌کنیم که سبب نمایش محتویات و اجزای آن خواهد شد. سپس بدون ایجاد هیچگونه تغییری در تنظیمات این صفحه، بر روی دکمه‌ی run httpbin کلیک می‌کنیم:


پس از اجرای خودکار مراحل این مجموعه، نتیجه‌ی عملیات ظاهر می‌شود:


در اینجا هر مرحله، پس از مرحله‌ای دیگر اجرا شده و اگر آزمایشی نیز به همراه آن‌ها بوده باشد نیز اجرا شده‌است. همچنین اگر بر روی نام هر کدام از مراحل کلیک کنیم، می‌توان جزئیات آن‌ها را نیز دقیقا بررسی کرد:


امکان اجرای یک چنین قابلیتی توسط برنامه‌ی CLI ای به نام newman نیز به همراه Postman وجود دارد که جهت استفاده‌ی در continuous integration servers می‌تواند بسیار مفید باشد. جزئیات آن‌را در قسمت‌های بعدی بررسی خواهیم کرد.
مطالب
MongoDb در سی شارپ (بخش هشتم)
در الگوهایی که به عنوان واسط بین اپلیکیشن و دیتابیس تعریف میکنیم نام دو الگوی Repository و Unit of work به چشم میخورد. در این سایت بارها این مباحث به صورت گفتمان و مقالات تکرار شده‌اند و میدانیم که این الگوها کمک شایانی برای بالا بردن کارآیی برنامه، عدم تکرار کد، قابلیت استفاده مجدد و راحتی کار برای آزمون‌های واحد و چهارچوب‌های تقلید میکنند.

Unit of Work یا الگوی کار در واقع یک الگو، جهت جمع آوری عملیات کار با دیتابیس است که همه عملیات را تحت یک تراکنش به سمت دیتابیس ارسال میکند تا مبحث Atomic بودن عملیات، به مرحله اجرا گذاشته شود. در صورتیکه یکی از عملیات با نقص یا خطایی روبرو شود، کل عملیات Roll back یا برگشت میخورد. از آنجا که دیتابیس‌های معدودی چون Ravendb این مراحل را تا حدی پیاده سازی میکنند نباید از مونگو هم چنین انتظاری نداشته باشید. مونگو برخورد تراکنشی یا اتمیک ندارد؛ پس پیاده سازی الگوی واحد کاری تاثیری بر روی روند کاری آن ندارد. هر چند تعدادی مثال بدین شکل پیاده شده‌اند، ولی در عمل حقیقی نیستند و تنها یک حرکت مشابه داشته‌اند.

ولی الگوی repository  برای پرهیز از تکرار کد، قابلیت به روزرسانی کد و همچنین عملیاتی چون آزمون‌های واحد و چهارچوب تقلید به کار میرود. وابستگی بین اشیاء را کاهش داده و باعث ایجاد یک کد با دوام‌تر میگردد.

ابتدا قبل از هر چیزی نیاز است تا اتصالات یا ساخت کانکشن به سرور و همچنین دریافت دیتابیس مورد نظر را در قالب یک کلاس تعریف نماییم. نام آن را MongoDbContext میگذارم:
   public class MongoDbContext : IMongoDbContext
    {
        public const string DatabaseName = "MongoDbTest";

        private static readonly IMongoClient _client;
        private static readonly IMongoDatabase Database;

        static MongoDbContext()
        {
            _client = new MongoClient();
            Database = _client.GetDatabase(DatabaseName);
        }

        public IMongoCollection<TEntity> GetCollection<TEntity>()
        {
            return Database.GetCollection<TEntity>(typeof(TEntity).Name.ToLower() + "s");
        }
    }
در حالت بالا شما میتوانید در سازنده کلاس اتصال را برقرار کرده و دیتابیس را دریافت نمایید و از متد GetCollection در سطوح بالاتر، نوع کالکشن درخواستی خود را اعلام کنید. اگر به خط اول کلاس دقت نمایید میبینید که ما از اینترفیسی به نام IMongoDbContext که شامل خطوط زیر میباشد استفاده کردیم و دلیل استفاده این است که اگر قرار باشد از کلاس کانتکست، در کلاس‌های repository استفاده شود، ایجاد وابستگی میکند. چرا که معلوم نیست این کانتکست دقیقا چیست و از کجا آمده است و در آزمون واحد و همچنین تقلید دست ما را می‌بندد و الگوی repository را مردود اعلام میکند. پس از این حیث یک اینترفیس با محتوای زیر تولید کرده‌ایم که از این پس از آن در کدها استفاده میکنیم و پر کردن این اینترفیس‌ها را از طریق تزریق وابستگی‌ها در حالت Constructor Injection که ساده‌ترین نوع آن است انجام میدهیم:
public interface IMongoDbContext
    {    
        IMongoCollection<TEntity> GetCollection<TEntity>();
    }
در صورتیکه دوست دارید محتوای‌های دیگری را چون کانکشن استرینگ و .. نیز در اینجا بگنجانید، به عهده خود شماست. تنها نیاز به تغییراتی کوچک است.
در مرحله بعد یک IMongoDbRepositry ساخته و محتوای آن را به شکل زیر پر میکنیم:
public interface IMongoDbRepository
{
Task<List<TEntity>> GetMany<TEntity>(FilterDefinition<TEntity> filter) where TEntity : class, new();
}
در اینجا کلاس‌ها را از نوع جنریک تعریف میکنیم تا کاربر بتواند هر نوع کلاسی را که نیاز دارد، به سمت این مخزن ارسال کند. در پیاده سازی هم به شکل زیر آن را تعریف میکنیم:
public class MongoRepository : IMongoDbRepository
    {

        private IMongoDbContext _mongoDbContext ;
        public MongoRepository(IMongoDbContext mongoDbContext)
        {
            _mongoDbContext = mongoDbContext;
        }
 public async Task<List<TEntity>> GetMany<TEntity>(FilterDefinition<TEntity> filter) where TEntity : class, new()
        {            
                var collection = GetCollection<TEntity>();
                var entities = await collection.Find(filter).ToListAsync();                
                return entities;
        }

 private IMongoCollection<TEntity> GetCollection<TEntity>()
        {
            return _mongoDbContext.GetCollection<TEntity>();
        } 
 }
همانطور که می‌بینید در ابتدا در سازنده از طریق یک کتابخانه‌ی تزریق وابستگی‌ها (که در اینجا من از Structure map استفاده کرده‌ام) شیء ImongoDbContext را مقدار دهی میکنیم. الان اگر در اینجا بجای تعریف اینترفیس، از همان کلاس مستقیما استفاده میکردیم، بین دو لایه repository و context یک وابستگی ایجاد میشد. ولی در اینجا کانتکست میتواند هر چیزی باشد. بعد از آن به تعریف متد مورد نظر پرداخته‌ایم. البته با توجه به اینکه این تنها یک مثال است، بنده تنها یکی از این متدها را به عنوان نمونه نشان داده‌ام و میتوانید فایل‌های کامل آن را در انتهای مقاله دریافت نمایید. همانطور که مشاهده میکنیم، متدها به صورت غیرهمزمان نوشته شده‌اند که باعث مقیاس پذیری برنامه میشوند و در اینجا از متدهای همزمان استفاده نکرده‌ایم؛ چرا که افرادی که از دیتابیس‌های غیر رابطه‌ای استفاده میکنند، نیاز به مقیاس پذیری بالایی دارند. به همین دلیل نیاز چندانی به استفاده از متدهای همزمان دیده نمیشود. ولی خودتان در صورت تمایل میتوانید آن‌ها را به اینترفیس اضافه کنید. در ضمن در کد بالا متد خصوصی را جهت دریافت کالکشن نوشته‌ایم تا دریافت کالکشن را در کدها، تا حدی خلاصه‌تر و شیواتر کنیم.

الگوی بالا در یک کنترلر به شرح زیر استفاده شده است:
 public class HomeController : Controller
    {
        private IMongoDbRepository _mongoDbRepository;

        public HomeController(IMongoDbRepository mongoDbRepository)
        {
            this._mongoDbRepository = mongoDbRepository;
        }
        // GET: Home
        public async Task<ActionResult> Index()
        {
            var filter = Builders<Resturant>.Filter.Gte("Capacity", 400);
            var c =await _mongoDbRepository.GetMany<Resturant>(filter);       
            return View(c);
        }
    }
در کد بالا رستورانهایی را که 400 نفر یا بیشتر ظرفیت پذیرایی دارند، واکشی کرده و در ویوو نشان میدهد. در اینجا الگوی repo، توسط تزریق وابستگی‌ها ساخته شده و کانتکست آن‌ها به همین شکل ساخته خواهد شد و در کل کنترلر، قابلیت استفاده را دارند.

  MongoRepository.zip
مطالب
ضمیمه کردن فایل در RavenDb
یکی از مواردی که در بانک‌های اطلاعاتی امروزه بیشتر مورد استفاده قرار میگیرد، ذخیره فایل‌ها در خود دیتابیس، بجای ذخیره نام یا آدرس آن‌ها بر روی دیسک سخت است. از همان ابتدا که Raven به بازار عرضه شد، امکان ذخیره فایل‌های باینری را با استفاده از افزونه هایی که به همراه داشت برای برنامه نویسان مهیا ساخت.  این امکان از طریق کد زیر برای ذخیره یک فایل کفایت میکرد:
using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    store.DatabaseCommands.PutAttachment(key: "file/1",
                                                         etag: null,
                                                         data: System.IO.File.OpenRead(@"D:\Prog\packages.config"),
                                                         metadata: new RavenJObject
                                                         {
                                                            { "Description", "توضیحات فایل" }
                                                         });
                    var question = new Question
                    {
                        By = "users/Vahid",
                        Title = "Raven Intro",
                        Content = "Test....",
                        FileId = "file/1"
                    };
                    session.Store(question);

                    session.SaveChanges();
                }
            }
ولی اگر از نسخه سه بعد RavenDb را استفاده کنید، می‌بینید که متد PutAttachment و دیگر خانواده این متد با ویژگی Obsolete (منسوخ شده) مزین شده‌اند و توصیه شده‌است از این پس از قابلیت جدیدی به نام RavenFs استفاده شود.
RavenFS یک فایل سیستم مجازی توزیع شده‌است و برای فایل‌های بزرگ چند گیگابایتی به طور بهینه‌ای طراحی گردیده‌است تا کارآیی بانک اطلاعاتی را بالا ببرد و وجود فایل‌های تکراری، از بین برود. این سیستم جدید شامل سیستم پیش فرض ایندکس گذاری میباشد و به شما این اجازه را میدهد تا بر روی متادیتاهای یک فایل از قبیل حجم، تاریخ آخرین نگارش و حتی متادیتاهای اختصاصی که شما در حین ذخیره سازی به آن اضافه می‌کنید، به جست‌وجو بپردازد. این سیستم جدید همچنین این امکان را به شما میدهد تا این اطلاعات را بین Node‌ها، با کمترین میزان انتقالات جابجا کنید و دسترسی سریعتری را بین نودهای مختلف داشته باشید.

برای ذخیره سازی یک فایل ابتدا باید یک FileStore را همانند آنچه که برای DocumentStore داشتید تعریف کنید. Url که شامل همان رشته اتصالی بوده و DefaultFileSystem هم  همانند DefaultDatabase که نام دیتابیس در آن ذکر میشد، در اینجا نام فایل سیستم ذکر می‌گردد:
 var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

بعد از آن باید از طریق Store جدید یک سشن ایجاد شود و فایل مورد نظر را در قالب یک استریم بخوانیم:
 var session = fileStore.OpenAsyncSession();
 var stream = File.OpenRead("D:\\Apocalypse.Now.Redux.1979.BDRip.YIFY.mkv");  

توجه داشته باشید که برای کار با فایل سیستم، همه متدهای session به صورت غیرهمزمان بوده و متد همزمانی وجود ندارد. سپس در مرحله بعد میخواهم متادیتاهای شخصی نیز به آن اضافه کنیم:
  var metadata = new RavenJObject
            {
                {"User", "users/1345"},
              {"Director","Francis Ford Coppola" }, 
                {"Year","1979" }
            };

با استفاده از شیء RavenObject میتوانیم در قالب کلید و مقدار، مقادیر خود را ذخیره کنیم و بعد از آن همه موارد بالا که شامل فایل هدر، استریم و متادیتای اختصاصی است را رجیستر کنیم. اگر هم چندین فایل داریم میتوانید آن‌ها را هم در همینجا رجیستر کنید:
session.RegisterUpload("mkv/sample.mkv", stream, metadata);

در مرحله بعدی تغییرات را تایید و عملیات آپلود آغاز میگردد:
await session.SaveChangesAsync();

همانطور که می‌بینید تمامی متدهای کاربردی این سشن به طور غیرهمزمان طراحی شده‌اند.
کلیه عملیاتی که در بالا انجام شد:
    var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

            var session = fileStore.OpenAsyncSession();
             var stream = File.OpenRead("D:\\Apocalypse.Now.Redux.1979.BDRip.YIFY.mkv"); 
            var metadata = new RavenJObject
            {
                {"User", "users/1345"},
               {"Director","Francis Ford Coppola" }, 
                {"Year","1979" }
            };

            session.RegisterUpload("Mkv/sample.mkv", stream, metadata);
            await session.SaveChangesAsync();

حالا اگر به نسخه سرور ravenDb مراجعه کنید می‌بینید که فایل طبق فایل هدر داده شده قرار گرفته است و اطلاعات مربوط به آن ذخیره شده است:



{
    "User": "users/1345",
    "Country": "Iran",
    "City": "Kashan",
    "Raven-Synchronization-History": [
        {
            "Version": 4,
            "ServerId": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4"
        },
        {
            "Version": 5,
            "ServerId": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4"
        }
    ],
    "Raven-Synchronization-Version": "6",
    "Raven-Synchronization-Source": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4"
}

برای خواندن هم به شیوه زیر عمل میکنیم:
 از طریق Store ایجاد شده، یک سشن جدید را باز می‌کنیم و فایل مورد نظر را از طریق یکی از متادیتاهای تعریف شده بازیابی میکنیم:
var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

            var session = fileStore.OpenAsyncSession();
            var file = await session.Query()
                   .WhereEquals("Year", "1979")
                   .FirstOrDefaultAsync();
سپس با آدرس دهی فایل هدر، فایل باینری را داخل استریم قرار می‌دهیم:
var stream = await session.DownloadAsync("mkv/"+file.Name);

سپس استریم را روی دیسک سخت دخیره یا به هر مکانی که مد نظر است ارسال می‌کنیم:
   var fs = File.Create("D:\\file2.mkv");
            stream.CopyTo(fs);
            fs.Flush();
            fs.Close();
البته از آنجائیکه عملیات بازیابی توسط بانک اطلاعاتی به صورت غیرهمزمان انجام می‌گیرد، بهتر هست که باقی عملیات هم به صورت غیرهمزمان انجام شود:
await stream.CopyToAsync(fs);

سپس کل کد بازیابی را به شکل زیر می‌نویسیم:
            var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

            var session = fileStore.OpenAsyncSession();
            var file = await session.Query()
                   .WhereEquals("Year", "1979")
                   .FirstOrDefaultAsync();

            var stream = await session.DownloadAsync("mkv/"+file.Name);

            var fs = File.Create("D:\\file2.mkv");

            await stream.CopyToAsync(fs);


مطالب
استفاده از GZip توکار IISهای جدید و تنظیمات مرتبط با آن‌ها
یکی از نقش‌های IISهای جدید (از نگارش 7 به بعد) که در ویندوز سرورهای قابل نصب است، نقش Performance است و ذیل آن دو نقش فشرده سازی استاتیک و پویا قابل انتخاب است. اگر این نقش‌ها بر روی سرور نصب باشند، دیگر نیازی به استفاده از HTTP Moduleهای متداول فشرده سازی صفحات وب نیست. برای استفاده‌ی از آن تنها کافی است کمی web.config را ویرایش کرد و ... گفته شده‌است که کار می‌کند! اما پس از اعمال تنظیمات، اگر به هدرهای خروجی Response صفحه در ابزارهای web developer مرورگرها دقت کنید، خبری از encoding جدیدی به نام gzip نیست (Content-Encoding: gzip) و به نظر اعمال نمی‌شود. در ادامه بررسی خواهیم کرد که چرا اینگونه است.


فعال سازی GZip توکار IIS

تنظیمات پیش فرض فعال سازی ماژول توکار GZip وب سرورهای جدید شامل دو مرحله است:
الف) تنظیمات سرور جهت فعال سازی فشرده سازی
بر روی ویندوزهای سرور، پس از مراجعه به Administrative Tools -> Server Manager و گشودن Roles آن، ذیل قسمت Web Server که در اینجا IIS است، نیاز است نقش جدیدی به نام Performance اضافه شود و مطابق تصویر، هر دو گزینه‌ی فشرده سازی استاتیک و پویا انتخاب گردد.


بنابراین اولین قدم برای عیب یابی کار نکردن GZip توکار IIS، از این مرحله شروع می‌شود که آیا اصلا ماژول مربوطه نصب هست یا خیر؟

ب) تنظیمات برنامه جهت فعال سازی ماژول GZip
پس از اطمینان از نصب ماژول توکار فشرده سازی صفحات وب IIS در سمت تنظیمات سرور، اکنون باید چند سطر ذیل را به Web.Config برنامه اضافه کرد:
  <system.webServer>

    <httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files">
      <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" />
      <dynamicTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/x-javascript" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="application/json" enabled="true" />
        <add mimeType="application/json; charset=utf-8" enabled="true" />
        <add mimeType="application/atom+xml" enabled="true" />
        <add mimeType="application/xaml+xml" enabled="true" />
        <add mimeType="*/*" enabled="false" />
      </dynamicTypes>
      <staticTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/x-javascript" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="application/json" enabled="true" />
        <add mimeType="application/json; charset=utf-8" enabled="true" />
        <add mimeType="application/atom+xml" enabled="true" />
        <add mimeType="application/xaml+xml" enabled="true" />
        <add mimeType="*/*" enabled="false" />
      </staticTypes>
    </httpCompression>
    <urlCompression doStaticCompression="true" doDynamicCompression="true" />

  </system.webServer>
در اینجا تنظیمات مخصوص نحوه‌ی فعال سازی فشرده سازی توکار صفحات پویا و فایل‌های استاتیک را مشاهده می‌کنید. در این تنظیمات محل قرارگیری فایل‌های موقتی فشرده شده‌ی توسط این ماژول و همچنین mime typeهای مدنظر جهت فشرده سازی، ذکر شده‌اند. با این تنظیمات، تنها mime typeهایی که به صورت صریح ذکر شده‌اند فشرده خواهند شد و از سایر mime types صرفنظر می‌شود.
این تنظیماتی است که در اکثر سایت‌ها نیز یافت می‌شود. ذکر آن‌ها اجباری است و پس از اعمال، اگر برنامه را اجرا کنید ... چیزی فشرده نمی‌شود! علت اصلی را باید در تنظیماتی یافت که مخصوص سرور است و در اینجا ذکر نشده‌اند.


تنظیمات مخصوص آستانه‌ی فشرده سازی صفحات

علت اصلی عدم مشاهده‌ی هدر gzip، در Response برنامه، به frequent hit threshold تنظیم شده‌ی در IIS بر می‌گردد. مقدار آن به 2 درخواست در طی 10 ثانیه تنظیم شده‌است. یعنی اگر به صفحه‌ای در طی 10 ثانیه دو درخواست نرسد، فشرده نخواهد شد. این تنظیم را می‌توان با مراجعه‌ی به configuration editor نود اصلی سرور وب در IIS manager، ویرایش کرد:



برای نمونه در تصویر فوق، این آستانه به یک درخواست در طی 10 ساعت تنظیم شده‌است. این عدد سبب خواهد شد تا تمامی درخواست‌های رسیده حتما فشرده سازی شوند.
این تنظیم معادل یک سطر ذیل در فایل web.config است. اما چون قسمت system.webServer/serverRuntime در تنظیمات سرور قفل شده‌است، هیچ تاثیری نخواهد داشت و حتما باید در سمت سرور و توسط IIS manager اعمال شود:
<system.webServer>
   <serverRuntime frequentHitThreshold="1" frequentHitTimePeriod="10:00:00" />
</system.webServer>
برای آزاد سازی این تنظیمات نیاز است دستور ذیل بر روی سرور اجرا شود. پس از آن کاربران برنامه‌های وب می‌توانند از تنظیمات وب کانفیگ خاص خود استفاده کنند:
 C:\Windows\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/serverRuntime

یک نکته
اگر از سرورهای پس از 2008 استفاده می‌کنید، گزینه‌ی staticCompressionIgnoreHitFrequency نیز به تنظیمات serverRuntime اضافه شده‌است که با تنظیم آن به true، از این حد آستانه، برای فایل‌های استاتیک صرفنظر خواهد شد.


تنظیمات مخصوص اندازه‌ی فایل‌هایی که باید فشرده سازی شوند

تنها حد آستانه‌ی درخواست صفحات وب نیست که بر روی فشرده سازی یا عدم آن ثاثیرگذار است. در اینجا میزان CPU Usage سیستم و یا حتی اندازه‌ی Response خروجی نیز مهم هستند که نمونه‌ای از تنظیمات آن‌را در شکل ذیل مشاهده می‌کنید:


در اینجا با تنظیم minFileSizeForComp به 1024، اعلام شده‌است که حجم‌هایی کمتر از یک کیلوبایت، فشرده سازی نشوند (مقدار پیش فرض آن، بیش از این عدد است).
البته این عدد را به شکل زیر نیز می‌توان به تنظیمات httpCompression وب کانفیگ اضافه کرد:
 <httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files" minFileSizeForComp="1024">

پس از اعمال این تنظیمات نیاز است یکبار IIS را نیز ری استارت کرد.


نتیجه گیری

اگر پس از فعال سازی GZip وب سرور، خروجی برنامه فشرده سازی نشد (Content-Encoding: gzip)، علت اینجا است که هنوز 2 درخواست مورد نیاز، در طی 10 ثانیه به سمت سرور ارسال نشده‌اند و تنظیمات پیش فرض این ماژول، جهت حداقل مصرف CPU و فشار بر روی سرور است.
مطالب
محدود کردن درخواست های Asp.Net Web Api بر اساس Client IP
در بسیاری از سناریو‌ها این موضوع مطرح می‌شود که سرویس‌های طراحی شده بر اساس Asp.Net Web Api، فقط به یک سری آی پی‌های مشخص سرویس دهند. برای مثال اگر Ip کلاینت در لیست کلاینت‌های دارای لایسنس خریداری شده بود، امکان استفاده از سرویس میسر باشد؛ در غیر این صورت خیر. بسته به نوع پیاده سازی سرویس‌های Web api، پیاده سازی این بخش کمی متفاوت خواهد شد. در طی این پست این موضوع را برای سه حالت IIs Host  و SelfHost و Owin Host بررسی می‌کنیم.
در اینجا قصد داریم حالتی را پیاده سازی نماییم که اگر درخواست جاری از سوی کلاینتی بود که Ip آن در لیست Ip‌های غیر مجاز قرار داشت، ادامه‌ی عملیات متوقف شود.

IIS Hosting:
حالت پیش فرض استفاده از سرویس‌های Web Api همین گزینه است؛ وابستگی مستقیم به System.Web . در مورد مزایا و معایب آن بحث نمی‌کنیم اما اگر این روش را انتخاب کردید تکه کد زیر این کار را برای ما انجام می‌دهد:
if (request.Properties.ContainsKey["MS_HttpContext"]) 
{
    var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper;
    if (ctx != null)
    {
        var ip = ctx.Request.UserHostAddress;       
    }
}
برای بدست آوردن شی HttpContext می‌توان آن را از لیست Properties‌های درخواست جاری به دست آورد. حال کد بالا را در قالب یک Extension Method در خواهیم آورد؛ به صورت زیر:

public static class HttpRequestMessageExtensions
{
    private const string HttpContext = "MS_HttpContext";

    public static string GetClientIpAddress(this HttpRequestMessage request)
    {
        if (request.Properties.ContainsKey(HttpContext))
        {
            dynamic ctx = request.Properties[HttpContext];
            if (ctx != null)
            {
                return ctx.Request.UserHostAddress;
            }
        }      
        return null;
    }
}

Self Hosting:

در حالت Self Host می‌توان عملیات بالا را با استفاده از خاصیت RemoteEndpointMessageProperty  انجام داد که تقریبا شبیه به حالت Web Host است. مقدار این خاصیت نیز در شی جاری HttpRequestMessage وجود دارد. فقط باید به صورت زیر آن را واکشی نماییم:
if (request.Properties.ContainsKey[RemoteEndpointMessageProperty.Name]) 
{
    var remote = request.Properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;

    if (remote != null)
    {
        var ip = remote.Address;        
    }
}
خاصیت RemoteEndpointMessageProperty  به تمامی درخواست‌ها وارده در سرویس‌های WCF چه در حالت استفاده از Http و چه در حالت Tcp اضافه می‌شود و در اسمبلی System.ServiceModel نیز می‌باشد. ار آنجا که Web Api از هسته‌ی WCF استفاده می‌کند (WCF Core) در نتیجه می‌توان از این روش استفاده نمود. فقط باید اسمبلی System.ServiceModel را به پروژه‌ی خود اضافه نمایید.

ترکیب حالت‌های قبلی:
اگر می‌خواهید کد‌های نوشته شده شما وابستگی به نوع هاست پروژه نداشته باشد، یا به معنای دیگر، در هر دو حالت به درستی کار کند می‌توانید به روش زیر حالت‌های قبلی را با هم ترکیب کنید. 
»در این صورت دیگر نیازی به اضافه کردن اسمبلی System.ServiceModel  نیست.
public static class HttpRequestMessageExtensions
{
    private const string HttpContext = "MS_HttpContext";
    private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty";

    public static string GetClientIpAddress(this HttpRequestMessage request)
    {
        if (request.Properties.ContainsKey(HttpContext))
        {
            dynamic ctx = request.Properties[HttpContext];
            if (ctx != null)
            {
                return ctx.Request.UserHostAddress;
            }
        }

        if (request.Properties.ContainsKey(RemoteEndpointMessage))
        {
            dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage];
            if (remoteEndpoint != null)
            {
                return remoteEndpoint.Address;
            }
        }

        return null;
    }
}
مرحله بعدی طراحی یک DelegatingHandler جهت استفاده از IP به دست آمده است .
 
public class MyHandler : DelegatingHandler
{
 private readonly HashSet<string> deniedIps;

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {

      if (deniedIps.Contains(request.GetClientIpAddress()))
        {            
            return Task.FromResult( new HttpResponseMessage( HttpStatusCode.Unauthorized ) );
        }

        return base.SendAsync(request, cancellationToken);
    }
}

Owin :

زمانی که از Owin   برای هاست سرویس‌های Web Api خود استفاده می‌کنید کمی روال انجام کار متفاوت خواهد شد. در این مورد نیز می‌توانید از DelegatingHandler‌ها استفاده کنید. معرفی DelegatingHandler طراحی شده به Asp.Net PipeLine به صورت زیر خواهد بود:
public class Startup
    {
        public void Configuration( IAppBuilder appBuilder )
        {     
            var config = new HttpConfiguration();
                 
            var routeHandler = HttpClientFactory.CreatePipeline( new HttpControllerDispatcher( config ), new DelegatingHandler[] 
            {
                new MyHandler(),
            } );
      
            config.Routes.MapHttpRoute(
                name: "Default",
                routeTemplate: "{controller}/{action}",
                defaults: null,
                constraints: null,
                handler: routeHandler
            );            
           
            config.EnsureInitialized();
         
            appBuilder.UseWebApi( config );         
        }
    }
اما نکته ای را که باید به آن دقت داشت، این است که یکی از مزایای استفاده از Owin، یکپارچه سازی عملیات هاستینگ قسمت‌های مختلف برنامه است. برای مثال ممکن است قصد داشته باشید که بخش هایی که با Asp.Net SignalR نیز پیاده سازی شده‌اند، قابلیت استفاده از کد‌های بالا را داشته باشند. در این صورت بهتر است کل عملیات بالا در قالب یک Owin Middleware عمل نماید تا تمام قسمت‌های هاست شده‌ی برنامه از کد‌های بالا استفاده نمایند؛ به صورت زیر:
public class IpMiddleware : OwinMiddleware
{
    private readonly HashSet<string> _deniedIps;

    public IpMiddleware(OwinMiddleware next, HashSet<string> deniedIps) :
        base(next)
    {
        _deniedIps = deniedIps;
    }

    public override async Task Invoke(OwinRequest request, OwinResponse response)
    {
        var ipAddress = (string)request.Environment["server.RemoteIpAddress"];

        if (_deniedIps.Contains(ipAddress))
        {
            response.StatusCode = 403;
            return;
        }

        await Next.Invoke(request, response);
    }
}
برای نوشتن یک Owin Middleware کافیست کلاس مورد نظر از کلاس OwinMiddleware ارث ببرد و متد Invoke را Override کنید.  لیست Ip‌های غیر مجاز، از طریق سازنده در اختیار Middleware قرار می‌گیرد. اگر درخواست مجاز بود از طریق دستور Next.Invoke(request,response) کنترل برنامه به مرحله بعدی منتقل می‌شود در غیر صورت عملیات با کد 403 متوقف می‌شود.
در نهایت برای معرفی این Middleware طراحی شده به Application، مراحل زیر را انجام دهید.
public class Startup
    {
        public void Configuration( IAppBuilder appBuilder )
        {       
           var config = new HttpConfiguration();                      

           var deniedIps = new HashSet<string> {"192.168.0.100", "192.168.0.101"}; 
          
app.Use(typeof(IpMiddleware), deniedIps); appBuilder.UseWebApi( config ); } }
مطالب
بررسی خطاهای متداول عملیات Migration در حین به روز رسانی پروژه‌های EF Code First

1. شاید یکی از آزاردهنده‌ترین مشکلات، برخورد با پیغام‌های خطا، هنگام عملیات migration باشد. یکی از ده‌ها نوع خطا، زمانی رخ می‌دهد که متد seed در حال اجراست. در این حالت هیچ نوع break-point ایی به کمک ما نخواهد آمد.

سوال ایجاست که آیا می‌توان این بخش را دیباگ نمود؟ بهترین راه حل، اجرای آپدیت از طریق متدها(یا اکشن ها) است.

فراخوانی migration بسیار ساده است. باید یک نمونه از کلاس Configuration را ساخته و در جایی از پروژه قرار دهیم و صد البته مطمئن باشیم که migration  فعال است. 

var configuration = new Configuration();
var migrator = new DbMigrator(configuration);
migrator.Update();

اگر بخواهید این تغییرات بر روی دیتابیسی با اسم و رسم انجام شود، از کد زیر بهره بگیرید: 

var configuration = new Configuration();
configuration.TargetDatabase = new DbConnectionInfo(
    "Server=MyServer;Database=MyDatabase;Trusted_Connection=True;", 
    "System.Data.SqlClient");
var migrator = new DbMigrator(configuration);
migrator.Update();
می‌توانید این کد را در ابتدای اکشن index در کنترلر Home قرار دهید و با قرار دادن Break-Point در بخش‌های مختلف متد Seed ، آن را بررسی کنید. 

2. خطای :

Duplicate type name within an assembly.

 معمولا بخاطر وجود break-point این مشکل رخ میدهد. یا break-pointهای درون seed را حذف کنید یا جای آنها را تغییر دهید.  [اینجا]

3. خطای : 

Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.

این مشکل میتواند دلایل مختلفی داشته باشد. کد درون متد seed را داخلtry/catch قرا ر دهید و علت آن را بررسی کنید:

                try
                {
                    var user = new ApplicationUser { UserName = "Admin", Phone = "09120000000", Email = "m@gmail.com" };
                    usermanager.Create(user, "09120000000");                                
                    usermanager.AddToRole(user.Id, "admin");
                }
                catch (DbEntityValidationException dbEx)
                {
                    foreach (var validationErrors in dbEx.EntityValidationErrors)
                    {
                        foreach (var validationError in validationErrors.ValidationErrors)
                        {
                            Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);
                        }
                    }
                }

فراموش نکنید، seed را به کمک حالتی که در شماره 1 گفته شد اجرا کنید تا بتوانید از BreakPoint استفاده کنید.

4.خطای : 

More than one context type was found in the assembly 'Ex1_CodeFirst'

این خطا وقتی ظاهر میشود که چندین Context برای پروژه جاری داشته باشیم و ویژوال استودیو نتواند Context مورد نظر ما را تشخیص دهد. بهتر است هنگام اجرای migration نام context مورد نظر را بنویسیم (مثلا MyConfiguration نام کانتکست شماست) :

Update-Database -ConfigurationTypeName MyConfiguration
5.خطای : 

There is already an object named 'UserProfile' in the database.

یعنی مدل شما پس از اینکه جدولی با همین نام (در این جا به عنوان مثال UserProfile) از پیش موجود بوده، تغییر کرده است؛ پس migration آپدیت شدنی نیست. ابتدا این دستور را می‌نویسیم (پیش از آنکه تغییراتمان را روی کلاس مربوط به جدول UserProfile اعمال کنیم): 

Add-Migration Initial –IgnoreChanges
اینکار باعث می‌شود یک فایل خالی به نام InitialMigration ایجاد شود. حالا تنظیمات مورد نظر، اعم از تغییر نام یا اضافه کردن ستون خاص را به کلاس UserProfile اعمال کرده و دیتابیس را آپدیت میکنیم :
update-database –verbose

گاهی این مشکل زمانی پیش می‌آید که واقعا تغییری در جدول نامبرده انجام نداده‌ایم. در این حالت روش پله‌ای زیر را به کار می‌بریم: 
        • پاک کردن یا ریست کردن migration (در شماره 9 همین مقاله این کار در چند مرحله توضیح داده شده است. در مرحله سوم حتما از Add-Migration Initial –IgnoreChanges استفاده کنید)  
        • بعد از آپدیت دیتابیس باید فایل زیر دارای محتویات باشد  

محتویات این فایل دقیقا شرایط فعلی جدول شماست.
        • اگر این فایل ایجاد نشده است مراحل را تکرار کنید تا محتویات درون آن را ببینید.
        • حالا تغییری را در یکی از مدل‌های خود انجام دهید. احتمالا با مشکل آپدیت شدن مواجه میشوید و پیغام زیر را دوباره خواهید دید: 
There is already an object named 'UserProfile' in the database
        • جدولی را که پیغام خطا به آن اشاره کرده، در فایل فوق بیابید و محدوده create آن را کامنت کنید تا ساخته نشود. 
        • دیتابیس را آپدیت کنید، احتمالا پیغام خطای فوق برای جدول دیگری نمایش داده می‌شود. آن را هم کامنت کنید و دیتابیس را آپدیت کنید و اگر باز هم خطا بود مکانیزم بالا را تا جایی تکرار کنید که خطایی نبینید .
        • حالا جدولی را که تغییراتی در آن داده بودید، در دیتابیس چک کنید که تغییرات اعمال شده باشد.
        • هر آنچه را در فایل initial کامنت کرده بودید، از کامنت خارج کنید و دیتابیس را آپدیت کنید .
        • برای آزمایش، یک آیتم به یکی از مدل‌ها اضافه کنید و ببینید که migration درست کار می‌کند یا خیر.
6.  خطای : 

Unable to update database to match the current model because there are pending changes 
and automatic migration is disabled. Either write the pending model changes to a code-based 
migration or enable automatic migration. 
Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true to enable automatic migration.
You can use the Add-Migration command to write the pending model changes to a code-based migration.
همانطور که از متن این پیغام پیداست، یعنی شما AutomaticMigration را true  نکرده‌اید و اینکار را باید در فایل  Configuration.cs  در فولدر Migration انجام داد. 

7.خطای : 
Automatic migration was not applied because it would result in data loss.
این خط را در سازنده‌ی کلاس Configuration اضافه میکنیم :

   AutomaticMigrationDataLossAllowed = true;
8.خطای : 

Introducing FOREIGN KEY constraint 'FK_dbo.ProductProductGroups_dbo.ProductGroups_ProductGroupId' on table 
'ProductProductGroups' may cause cycles or multiple cascade paths. 
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
خطای فوق وقتی رخ میدهد که دو جدول از یک طرف به هم وصل باشند و از طرف دیگر مشخصات ذکر نشده باشد.
9. راه حل برای خطاهای عجیبی که شاید نیاز به صرف زمان بیشتر برای کشف و برطرف کردن داشته باشند : 
بهتر نیست مایگریشن خود را از نو بسازید؟
روش به روز رسانی و بازسازی migration [اینجا ]:
- پاک کردن فولدر Migrations در پروژه
- پاک کردن جدولی به نام MigrationHistory_  در دیتابیس (ممکن است زیر مجموعه جدول‌های system باشد)
- اجرای دستور زیر کنسول پکیچ منیجر ویژوال استودیو :
 Enable-Migrations -EnableAutomaticMigrations -Force
- اجرای دستور زیر :
 Add-Migration Initial