مطالب
آشنایی با Gridify
Gridify چیست ؟

به طور خلاصه Gridify یک کتابخانه ساده و سریع است که عملیات‌های Filtering , Pagination و Sorting را با استفاده از شرط‌های متنی (string based) امکان پذیر میکند.
به طور مثال فرض کنید که یک API را برای دریافت لیست کاربران با نام UsersList نوشته‌اید. مثال:
 [HttpGet("[action]")]
 public async Task<IActionResult> UsersList()
 {
    var users =  await _dbContext.Users.AsNoTracking().ToListAsync();
    return Ok(users);
 }
طبیعتا بخش FrontEnd نرم افزار شما نیاز دارد این اطلاعات را به کاربر نمایش دهد. به همین جهت در بیشتر مواقع از یک گرید برای نمایش این اطلاعات استفاده میشود.
پس از دریافت اطلاعات از سرور با مشکلات زیر مواجه خواهیم شد.
  1. عدم پشتیبانی از Pagination: چون API، تمامی کاربران را به سمت کلاینت ارسال میکند؛ به همین جهت، هم با مشکل کارآیی (performance) در آینده مواجه میشویم و هم امکان گذاشتن صفحه بندی (Pagination) وجود نخواهد داشت. 
  2. عدم پشتیبانی از Sorting: اگر در گرید نمایش داده شده کاربر بخواهد اطلاعات را Sort کند، چون چنین امکانی هنوز برای API ما تعریف نشده، این عملیات سمت سرور امکان پذیر نیست.
  3. عدم پشتیبانی از Filtering: همیشه نمایش تمامی اطلاعات مفید نیست. در اکثر مواقع ما نیاز داریم تا قسمتی از اطلاعات را با شرطی خاص، برگردانیم. به طور مثال لیست کاربران فعال در سامانه یا لیست کاربران غیرفعال. 
این مشکلات بدون استفاده از هیچ کتابخانه‌ای قابل حل است ولی نه به سادگی؛ به طور مثال یا باید چندین API مختلف با امکانات مختلف بنویسیم، یا یک API را برای پشتیبانی از این موارد تغییر بسیار دهیم. من برای اینکه از بحث دور نشویم، به پیاده سازی نمونه دستی پشتیبانی از این موارد در اینجا نمی‌پردازم، چرا که اگر یکبار تلاشی را برای اینکار انجام داده باشیم، طبیعتا  مشکلات و کد کثیفی که در نهایت تولید شده است، یادآوری خواهد شد. 
برای رفع این مشکلات میتوان از کتابخانه Gridify استفاده کرد. مثال :
 [HttpGet("[action]")]
 public async Task<IActionResult> UsersList(GridifyQuery filter)
 {
    var users =  await _dbContext.Users.AsNoTracking().GridifyAsync(filter);
    return Ok(users);
 }
در مثال بالا با استفاده از کلاس GridifyQuery میتوانیم به کنترل هر سه مشکل Sorting - Pagination - Filtering سمت کلاینت بپردازیم. (در ادامه با این کلاس بیشتر آشنا خواهیم شد).


استفاده از Gridify به API‌ها محدود نمیشود. به طور کلی ما میتوانیم در هر نوع لیستی که امکان استفاده از IQueryable  را به ما میدهد از آن استفاده نماییم. 
فرض کنید در یک برنامه Console Application قصد داریم یک فیلتر را از کاربر دریافت کرده و آن را روی لیست خروجی خود اعمال کنیم. به دلیل اینکه امکان جستجوی متنی در دات نت وجود ندارد، برای انجام اینکار مجبور خواهیم شد که برای تک تک فیلدهایی که قرار است برای فیلترینگ پشتیبانی کنیم، یک query جداگانه بنویسیم؛ ولی این عملیات توسط کتابخانه Gridify امکان پذیر است. به طور مثال فرض کنید قصد داریم در لیست کاربران، کاربرانی را  با نام Ali، پیدا کنیم. 
var result = Users.AsQueryable().ApplyFiltering("name==Ali");
این کد دقیقا معادل کد زیر است.
var result = Users.AsQueryable().Where(q => q.Name == "Ali");
در اینجا با استفاده از کتابخانه Gridify ما توانستیم یک static Linq را به یک dynamic Linq که در runtime مقدار دهی خواهد شد، تغییر دهیم. به همین جهت استفاده از مورد اول در برنامه‌ی Console ما امکان پذیر است. تا اینجا ما با امکانات کلی این کتابخانه آشنا شدیم. در مقالات بعدی سعی میکنم به سایر امکانات این کتابخانه و بیشتر به جزئیات بپردازم. همینطور برای کسب اطلاعات بیشتر میتوانید به لینک زیر مراجعه نمایید.
مطالب
آشنایی با FileTable در SQL Server 2012 بخش 2
ستون دیگر stream_id نام دارد که از نوع uniqueidentifier ROWGUIDCOL است. همان‌گونه که در یاد دارید، در FileStream نیز ناگزیر به تعریف چنین ستونی بودیم. بنابراین FileTable استثناء نیست و در این‌جا نیست چنین فیلدی توسط SQL Server تعریف می‌شود. اگر فایل‌ها و پوشه‌ها جابه‌جا نمی‌شدند می‌توانستید از هر دو ستون path_locator یا stream_id برای شناسایی یک رکورد از جدول بهره ببرید. ولی با جابه‌جایی یک فایل و یا به عبارت دیگر تغییر پدر آن در ساختار سلسله‌مراتبی، مقدار path_locator نیز تغییر می‌کند، پس ناگزیر به استفاده از این ستون برای ارجاع به یک ردیف در جدول هستیم.
هر ردیف از جدول نمایان‌گر یک فایل یا پوشه است، بنابراین به ستونی نیاز داریم که بتوانیم این موضوع را نشان دهیم. بر این پایه از ستون is_directory بهره می‌بریم که 1 بودن آن نشان‌دهنده‌ی این است که این ردیف از جدول به یک پوشه ارجاع دارد.
نام فایل یا پوشه در ستونی به نام name نگه‌داری می‌شود که رشته‌ای از نوع (nvarchar(255 است. افزون بر این ستون، ستون‌های دیگری نیز در این جدول وجود دارد که ویژگی‌های یک فایل مانند پنهان‏‌بودن، فقط‏‌خواندنی و ... توسط آن توسط آن به دست می‏آید. ستون پسین file_stream نام دارد که برای پوشه‌ها، محتوای آن Null است. علت آن این است که محتوای واقعی فایل در این ستون نگه‌داری می‌شود. در واقع یک (varbinary(max با ویژگی‌های fileStream است که محتوای باینری آن در سیستم فایل NTFS ذخیره می‌شود. مدیریت پشت صحنه‌ی این ستون برعهده‌ی SQL Server است.
افزون بر این 14 ستون، هر FileTable شامل سه ستون محاسباتی به شرح زیر است:

ستون parent_path_locator نتیجه‏‌ی فراخوانی تابع (GetAncestor(1 در ستون path_locator است که جهت به دست آوردن پوشه‏‌ی پدر یک فایل و پوشه استفاده می‏‌شود. ستون file_type که از مقدار رشته‏‌ای ستون name تجزیه شده است، پسوند فایل را برمی‏‌گرداند. و ستون cached_file_size اندازه‌ی بایت ذخیره‏‌شده ستون file_stream  را برمی‏‌گرداند. با این ساختار ثابت در اینجا، هر FileTable هر آن‏چه از File System نیاز دارید در یک پوشه‏ی اشتراکی به شما می‏‌دهد.

این یعنی نمایش بی‏‌واسطه FileTable به هر کاربر یا برنامه. به طوری که برای نمایش یا به‏‌روزرسانی جدول می‌توانید از روش استاندارد I/O مانند کشیدن و رهاکردن با Windows Explorer یا برنامه‏‌نویسی با  System.IO.FileStream  و API‌های ویندوز استفاده کنید. این‏‌چنین:

- ایجاد یک فایل یا پوشه در سیستم فایل  -> افزودن یک ردیف به جدول

- افزودن یک ردیف به جدول -> ایجاد یک فایل یا پوشه در سیستم فایل 

با کپی فایل‌ها در مسیر بالا، به صورت خودکار رکوردهای زیر در جدول PhotoTable در پایگاه‌داده‌ها افزوده می‌شود: 

به طور خلاصه پیش از این برای افزودن به FileStream دو راه کار پیش رو داشتید. یکی استفاده از T-SQL و دیگر sqlFileStream اکنون SQL Server 2012 راه کار سوم را پیشنهاد می‌کند. استفاده از File System در این روش FileStream  به طور خودکار پر می‌شود. 

پیش از ساخت یک FileTable بیان این نکته دارای اهمیت است که با کپی فایل‏‌ها و پوشه‏‌ها هیچ چیز جدیدی به NTFS افزوده نمی‌شود بلکه محتوای فایل به FileStream افزوده می‌شود و SQL Server با بررسی همزمان FileStream و FileTable نمایشی از ردیف‏‌های FileTable به صورت یک پوشه‏‌ی اشتراکی نشان می‌دهد. این نکته پاسخی به این پرسش خواهد بود که آیا با استفاده از FileTable حجم پایگاه‏‌داده‏‌ها دو برابر خواهد شد و در نتیجه دشواری‏‌ها و چالش‏‌های نگه‏داری و پشتیبانی را پیش رو خواهیم داشت!؟ که پاسخ "خیر" خواهد بود. 

ایجاد یک  FileTable

پیش از این در همین تارنما، روش فعال کردن FileStream در SQL Server  را آموزش دیده اید. اگر درست به خاطر داشته باشید، چیزی شبیه به دستورهای زیر بود:

CREATE DATABASE MyFileArchive
ON PRIMARY
(NAME = MyFileArchive_data,
FILENAME = 'C:\Demo\MyFileArchive_data.mdf'),
FILEGROUP FileStreamGroup CONTAINS FILESTREAM
(NAME = PhotoFileLibrary_blobs,
FILENAME = 'C:\Demo\MyFiles')
LOG ON
(NAME = PhotoFileLibrary_log,
FILENAME = 'C:\Demo\MyFileArchive_log.ldf')

FileTable  به FileStream متکی است؛ بر این پایه پیش از ایجاد یک FileTable باید FileStream را روی پایگاه‌داده‌ها فعال کنیم. این کار با یک تعریف درست توسط بند FILEGROUP…CONTAINS FILESTREAM انجام می‌شود. 

برای ایجاد FileTable تنها کافی است که بند WITH FILESTREAM را به دستور CREATE DATABASE بیفزایید. (یا برای فعال‌کردن FileTable روی یک پایگاه‌داده‌ی ساخته شده بند SET FILESTREAM را در دستور ALTER DATABASE  بنویسید.) در این بند، از DIRECTORY_NAME برای نام‌گذاری یک پوشه برای پایگاه‌داده‌ها استفاده می‌کنیم. این پوشه در یک پوشه ریشه به نام SQL Server instance نمایش داده خواهد شد. بخش دوم بند NON_TRANSACTED_ACCESS=FULL  است که دسترسی غیرتراکنشی را فعال می‌کند.  با این کار برای هر FileTable  در پایگاه داده یک زیرپوشه درون پوشه‌ای که به نام DIRECTORY_NAME  نام‌گذاری شده است؛ ساخته می‌شود. 

با توجه به آنچه گفته شد برای ایجاد یک پایگاه‌داده با امکان ساخت FileTable دستورهای زیر را اجرا کنید: 

CREATE DATABASE MyFileArchive
ON PRIMARY
(NAME = MyFileArchive_data,
FILENAME = 'C:\Demo\MyFileArchive_data.mdf'),
FILEGROUP FileStreamGroup CONTAINS FILESTREAM
(NAME = PhotoFileLibrary_blobs,
FILENAME = 'C:\Demo\MyFiles')
LOG ON
(NAME = PhotoFileLibrary_log,
FILENAME = 'C:\Demo\MyFileArchive_log.ldf')
WITH FILESTREAM
(DIRECTORY_NAME='FilesLibrary',
NON_TRANSACTED_ACCESS=FULL)
اکنون برای ساخت یک FileTable درون این پایگاه‌داده‌ها از دستور زیر استفاده کنید:
USE MyFileArchive
GO
CREATE TABLE PhotoTable AS FileTable
GO
توجه داشته باشید که چون ستون‌های FileTable از پیش تعریف شده است؛ ایجاد آن فقط با نوشتن دستور امکان پذیر است و مانند یک Table عادی از محیط کاربری SQL Server نمی‌توان بهره برد. 
در Object Explorer از گره‏‌ی Tables، گره‏‌ی FileTables را باز کنید و روی جدولی که هم‌‏اکنون ساختیم راست‏‌کلیک کنید. با انتخاب گزینه‏‌ی Explore FileTable Directory پنجره‏‌ی زیر بازمی‏‌شود:

دنباله دارد ...

مطالب دوره‌ها
تراکنش‌ها در RavenDB
پیش از شروع به بحث در مورد تراکنش‌ها و نحوه مدیریت آن‌ها در RavenDB، نیاز است با مفهوم ACID آشنا شویم.

ACID چیست؟

ACID از 4 قاعده تشکیل شده است (Atomic, Consistent, Isolated, and Durable) که با کنار هم قرار دادن آن‌ها یک تراکنش مفهوم پیدا می‌کند:

الف) Atomic: به معنای همه یا هیچ
اگر تراکنشی از چندین تغییر تشکیل می‌شود، همه‌ی آن‌ها باید با موفقیت انجام شوند، یا اینکه هیچکدام از تغییرات نباید فرصت اعمال نهایی را بیابند.
برای مثال انتقال مبلغ X را از یک حساب، به حسابی دیگر درنظر بگیرید. در این حالت X ریال از حساب شخص کسر و X ریال به حساب شخص دیگری واریز خواهد شد. اگر موجودی حساب شخص، دارای X ریال نباشد، نباید مبلغی از این حساب کسر شود. مرحله اول شکست خورده است؛ بنابراین کل عملیات لغو می‌شود. همچنین اگر حساب دریافت کننده بسته شده باشد نیز نباید مبلغی از حساب اول کسر گردد و در این حالت نیز کل تراکنش باید برگشت بخورد.

ب) Consistent یا یکپارچه
در اینجا consistency علاوه بر اعمال قیود، به معنای اطلاعاتی است که بلافاصله پس از پایان تراکنشی از سیستم قابل دریافت و خواندن است.

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

د) Durable: ماندگار
اگر سیستم پایان تراکنشی را اعلام می‌کند، این مورد به معنای 100 درصد نوشته شدن اطلاعات در سخت دیسک باید باشد.


مراحل چهارگانه ACID در RavenDB به چه نحوی وجود دارند؟

RavebDB از هر دو نوع تراکنش‌های implicit و explicit پشتیبانی می‌کند. Implicit به این معنا است که در حین استفاده معمول از RavenDB (و بدون انجام تنظیمات خاصی)، به صورت خودکار مفهوم تراکنش‌ها وجود داشته و اعمال می‌شوند. برای نمونه به متد ذیل توجه نمائید:
public void TransferMoney(string fromAccountNumber, string toAccountNumber, decimal amount) 
{
   using(var session = Store.OpenSession()) 
   {
         session.Advanced.UseOptimisticConcurrency = true;

         var fromAccount = session.Load<Account>("Accounts/" + fromAccountNumber);
         var toAccount = session.Load<Account>("Accounts/" + toAccountNumber);

         fromAccount.Balance -= amount;
         toAccount.Balance += amount;

         session.SaveChanges();
   }
}
در این متد مراحل ذیل رخ می‌دهند:
- از document store ایی که پیشتر تدارک دیده شده، جهت بازکردن یک سشن استفاده شده است.
- به سشن صراحتا عنوان شده است که از Optimistic Concurrency استفاده کند. در این حالت RavenDB اطمینان حاصل می‌کند که اکانت‌های بارگذاری شده توسط متدهای Load، تا زمان فراخوانی SaveChanges تغییر پیدا نکرده‌اند (و در غیراینصورت یک استثناء را صادر می‌کند).
- دو اکانت بر اساس Id آن‌ها از بانک اطلاعاتی واکشی می‌شوند.
- موجودی یکی تقلیل یافته و موجودی دیگر، افزایش می‌یابد.
- متد SaveChanges بر روی شی‌ء سشن فراخوانی شده است. تا زمانیکه این متد فراخوانی نشده است، کلیه تغییرات در حافظه نگهداری می‌شوند و به سرور ارسال نخواهند شد. فراخوانی آن سبب کامل شدن تراکنش و ارسال اطلاعات به سرور می‌گردد.
بنابراین شیء سشن بیانگر یک atomic transaction ماندگار و محصور شده است (سه جزء ACID تاکنون محقق شده‌اند). محصور شده بودن آن به این معنا است که:
الف) هر تغییری که در سشن اعمال می‌شود، تا پیش از فراخوانی متد SaveChanges از دید سایر تراکنش‌ها مخفی است.
ب) اگر دو تراکنش همزمان رخ دهند، تغییرات هیچکدام بر روی دیگری اثری ندارد.

اما Consistency یا یکپارچگی در RavenDB بستگی دارد به نحوه‌ی خواندن اطلاعات و این مورد با دنیای رابطه‌ای اندکی متفاوت است که در ادامه جزئیات آن‌را بیشتر بررسی خواهیم کرد.


عاقبت یک دست شدن یا eventual consistency

درک Consistency مفهوم ACID در RavenDB بسیار مهم است و عدم آشنایی با نحوه عملکرد آن می‌تواند مشکل‌ساز شود. در دنیای بانک‌های اطلاعاتی رابطه‌ای، برنامه نویس‌ها به «immediate consistency» عادت دارند (یکپارچگی آنی). به این معنا که هرگونه تغییری در بانک اطلاعاتی، پس از پایان تراکنش، بلافاصله در اختیار کلیه خوانندگان سیستم قرار می‌گیرد. در RavenDB و خصوصا دنیای NoSQL، این یکپارچگی آنی دنیای رابطه‌ای، به «eventual consistency» تبدیل می‌شود (عاقبت یک‌دست شدن). عاقبت یک دست شدن در RavenDB به این معنا است که اگر تغییری به یک سند اعمال گردیده و ذخیره شود؛ کوئری انجام شده بر روی این اطلاعات تغییر یافته ممکن است «stale data» باز گرداند. واژه stale در RavenDB به این معنا است که هنوز اطلاعاتی در دیتابیس موجود هستند که جهت تکمیل ایندکس‌ها پردازش نشده‌اند. به این مورد در قسمت بررسی ایندکس‌ها در RavenDB اشاره شد.
در RavenDB یک سری تردهای پشت صحنه، مدام مشغول به کار هستند و بدون کند کردن عملیات سیستم، کار ایندکس کردن اطلاعات را انجام می‌دهند. هر زمانیکه اطلاعاتی را ذخیره می‌کنیم، بلافاصله این تردها تغییرات را تشخیص داده و ایندکس‌ها را به روز رسانی می‌کنند. همچنین باید درنظر داشت که RavenDB جزو معدود بانک‌های اطلاعاتی است که خودش را بر اساس نحوه استفاده شما ایندکس می‌کند! (نمونه‌ای از آن‌را در قسمت ایندکس‌های پویای حاصل از کوئری‌های LINQ پیشتر مشاهده کرده‌اید)

نکته مهم
در RavenDB اگر از کوئری‌های LINQ استفاده کنیم، ممکن است به علت اینکه هنوز تردهای پشت صحنه‌ی ایندکس سازی اطلاعات، کارشان تمام نشده است، تمام اطلاعات یا آخرین اطلاعات را دریافت نکنیم (که به آن stale data گفته می‌شود). هر آنچه که ایندکس شده است دریافت می‌گردد (مفهوم عاقبت یک دست شدن ایندکس‌ها). اما اگر نیاز به یکپارچگی آنی داشتیم، متد Load یک سشن، مستقیما به بانک اطلاعاتی مراجعه می‌کند و اطلاعات بازگشت داده شده توسط آن هیچگاه احتمال stale بودن را ندارند.
بنابراین برای نمایش اطلاعات یا گزارشگیری، از کوئری‌های LINQ استفاده کنید. RavenDB خودش را بر اساس کوئری شما ایندکس خواهد کرد و نهایتا به کوئری‌هایی فوق العاده سریعی در طول کارکرد سیستم خواهیم رسید. اما در صفحه ویرایش اطلاعات بهتر است از متد Load استفاده گردد تا نیاز به مفهوم immediate consistency یا یکپارچگی آنی برآورده شود.


تنظیمات خاص کار با ایندکس سازها برای انتظار جهت اتمام کار آن‌ها

عنوان شد که اگر ایندکس سازهای پشت صحنه هنوز کارشان تمام نشده است، در حین کوئری گرفتن، هر آنچه که ایندکس شده بازگشت داده می‌شود.
در اینجا می‌توان به RavenDB گفت که تا چه زمانی می‌تواند یک کوئری را جهت دریافت اطلاعات نهایی به تاخیر بیندازد. برای اینکار باید اندکی کوئری‌های LINQ آن‌را سفارشی سازی کنیم:
RavenQueryStatistics stats;
var results = session.Query<Product>()
    .Statistics(out stats)
    .Where(x => x.Price > 10)
    .ToArray();
 
if (stats.IsStale)
{
    // Results are known to be stale
}
توسط امکانات آماری کوئری‌های LINQ در RavenDB مطابق کدهای فوق، می‌توان دریافت که آیا اطلاعات دریافت شده stale است یا خیر.
همچنین زمان انتظار تا پایان کار ایندکس ساز را نیز توسط متد Customize به نحو ذیل می‌توان تنظیم کرد:
RavenQueryStatistics stats;
var results = session.Query<Product>()
    .Statistics(out stats)
    .Where(x => x.Price > 10)
    .Customize(x => x.WaitForNonStaleResults(TimeSpan.FromSeconds(5)))
    .ToArray();
به علاوه می‌توان کلیه کوئری‌های یک documentStore را وارد به صبر کردن تا پایان کار ایندکس سازی کرد (متد Customize پیش فرضی را با WaitForNonStaleResultsAsOfLastWrite مقدار دهی و اعمال می‌کند):
 documentStore.Conventions.DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites;
این مورد در برنامه‌های وب توصیه نمی‌شود چون کل سیستم در حین آغاز کار با آن بر اساس یک documentStore سینگلتون باید کار کند و همین مساله صبر کردن‌ها، با بالا رفتن حجم اطلاعات و تعداد کاربران، پاسخ دهی سیستم را تحت تاثیر قرار خواهد داد. به علاوه این تنظیم خاص بر روی کوئری‌های پیشرفته Map/Reduce کار نمی‌کند. در این نوع کوئری‌های ویژه، برای صبر کردن تا پایان کار ایندکس شدن، می‌توان از روش زیر استفاده کرد:
while (documentStore.DatabaseCommands.GetStatistics().StaleIndexes.Length != 0)
{
    Thread.Sleep(10);
}

مقابله با تداخلات همزمانی

با تنظیم session.Advanced.UseOptimisticConcurrency = true، اگر سندی که در حال ویرایش است، در این حین توسط کاربر دیگری تغییر کرده باشد، استثنای ConcurrencyException صادر خواهد شد. همچنین این استثناء در صورتیکه شخصی قصد بازنویسی سند موجودی را داشته باشد نیز صادر خواهد شد (شخصی بخواهد سندی را با ID سند موجودی ذخیره کند). اگر از optimistic concurrency استفاده نشود، آخرین ترد نویسنده یا به روز کننده اطلاعات، برنده خواهد شد و اطلاعات نهایی موجود در بانک اطلاعاتی متعلق به او و حاصل بازنویسی آن ترد است.
 optimistic concurrency به زبان ساده به معنای به خاطر سپردن شماره نگارش یک سند است، زمانیکه آن‌را بارگذاری می‌کنیم و سپس ارسال آن به سرور، زمانیکه قصد ذخیره آن‌را داریم. در SQL Server اینکار توسط RowVersion انجام می‌شود. در بانک‌های اطلاعاتی سندگرا چون تمایل به استفاده از HTTP در آن‌ها زیاد است (مانند RavenDB) از مکانیزمی به نام E-Tag برای این منظور کمک گرفته می‌شود. هر زمانیکه تغییری به یک سند اعمال می‌شود، E-Tag آن  به صورت خودکار افزایش خواهد یافت.
برای مثال فرض کنید کاربری سندی را با E-Tag مساوی 2 بارگذاری کرده است. قبل از اینکه این کاربر در صفحه ویرایش اطلاعات کارش با این سند خاتمه یابد، کاربر دیگری در شبکه، این سند را ویرایش کرده است و اکنون E-Tag آن مثلا مساوی 6 است. در این زمان اگر کاربر یک سعی به ذخیره سازی اطلاعات نماید، چون E-Tag سند او با E-Tag سند موجود در سرور دیگر یکی نیست، با استثنای ConcurrencyException متوقف خواهد شد.



مشکل! در برنامه‌های بدون حالت وب، چون پس از نمایش صفحه ویرایش اطلاعات، سشن RavenDB نیز بلافاصله Dispose خواهد شد، این E-Tag را از دست خواهیم داد. همچنین باید دقت داشت که سشن RavenDB به هیچ عنوان نباید در طول عمر یک برنامه باز نگهداشته شود و برای طول عمری کوتاه طراحی شده است. راه حلی که برای آن درنظر گرفته شده است، ذخیره سازی این E-Tag در بار اول دریافت آن از سشن می‌باشد. برای این منظور تنها کافی است خاصیتی را به نام Etag با ویژگی JsonIgnore (که سبب عدم ذخیره سازی آن در بانک اطلاعاتی خواهد شد) تعریف کنیم:
public class Person
{
    public string Id { get; set; }

    [JsonIgnore]
    public Guid? Etag { get; set; }

    public string Name { get; set; }
}
اکنون زمانیکه سندی را از بانک اطلاعاتی دریافت می‌کنیم، با استفاده از متد session.Advanced.GetEtagFor، می‌توان این Etag واقعی را دریافت کرد و ذخیره نمود:
public Person Get(string id)
{
    var person = session.Load<Person>(id);
    person.Etag = session.Advanced.GetEtagFor(person);
    return person;
}
و برای استفاده از آن ابتدا باید UseOptimisticConcurrency به true تنظیم شده و سپس در متد Store این Etag دریافتی از سرور را مشخص نمائیم:
public void Update(Person person)
{
    session.Advanced.UseOptimisticConcurrency = true;
    session.Store(person, person.Etag, person.Id);
    session.SaveChanges();
    person.Etag = session.Advanced.GetEtagFor(person);
}


تراکنش‌های صریح

همانطور که عنوان شد، به صورت ضمنی کلیه سشن‌ها، یک واحد کار را تشکیل داده و با پایان آن‌ها، تراکنش خاتمه می‌یابد. اگر به هر علتی قصد تغییر این رفتار ضمنی پیش فرض را دارید، امکان تعریف صریح تراکنش‌های نیز وجود دارد:
using (var transaction = new TransactionScope())
{
   using (var session1 = store.OpenSession())
   {
     session1.Store(new Account());
     session1.SaveChanges();
   }

   using (var session2 = store.OpenSession())
   {
     session2.Store(new Account());
     session2.SaveChanges();
   }

   transaction.Complete();
}
باید دقت داشت که پایان یک تراکنش، یک non-blocking asynchronous call است و مباحث stale data که پیشتر در مورد آن بحث شد، برقرار هستند.
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت هشتم - بررسی عملگرهای Hash Join و Compute Scalar در یک Query Plan
در یک hash join، اطلاعات از دو ورودی نامرتب، دریافت و join می‌شوند که نسبت به merge join، عملیات سنگین‌تری است. برای اینکار، یک hash table را از دیتاست خارجی و یک نمونه‌ی دیگر را بر اساس دیتاست درونی ساخته و سپس کار انطباق ردیف‌ها را انجام می‌دهد.


بررسی عملگر hash join

 ابتدا در management studio از منوی Query، گزینه‌ی Include actual execution plan را انتخاب می‌کنیم. سپس کوئری‌های زیر را اجرا می‌کنیم:
USE [WideWorldImporters];
GO

SET STATISTICS IO ON;
GO


/*
Query with a hash join
*/
SELECT
    [ol].[OrderID],
    [ol].[OrderLineID],
    [ol].[StockItemID],
    [ol].[PickedQuantity],
    [si].[StockItemName],
    [si].[UnitPrice]
FROM [Warehouse].[StockItems] [si]
    JOIN [Sales].[OrderLines] [ol]
    ON [si].[StockItemID] = [ol].[StockItemID];
GO
در اینجا اطلاعات دو جدول StockItems و OrderLines بر روی ستون StockItemID با هم Join شده‌اند و اجرای آن یک چنین کوئری پلنی را تولید می‌کند:


دیتاست بالایی که ضخامت پیکان خارج شده‌ی از آن کمتر است، تعداد ردیف‌های کمتری را نسبت به دیتاست درونی دارد (227 ردیف، در مقابل بیش از 231 هزار ردیف).
با حرکت اشاره‌گر ماوس بر روی هر کدام از ایندکس‌ها، می‌توان با دقت کردن به Output List آن‌ها، دقیقا دریافت که هرکدام، چه ستون‌هایی از کوئری نهایی را تامین می‌کنند:
دیتاست بالایی که از PK_Warehouse_StockItems تامین می‌شود:
ALTER TABLE [Warehouse].[StockItems] ADD  CONSTRAINT [PK_Warehouse_StockItems] PRIMARY KEY CLUSTERED
(
   [StockItemID] ASC
)


دیتاست درونی که از NCCX_Sales_OrderLines تامین می‌شود و یک COLUMNSTORE INDEX است:
CREATE NONCLUSTERED COLUMNSTORE INDEX [NCCX_Sales_OrderLines] ON [Sales].[OrderLines]
(
[OrderID],
[StockItemID],
[Description],
[Quantity],
[UnitPrice],
[PickedQuantity]
)



بهبود کارآیی hash join با فشرده سازی ایندکس‌های آن

ایندکس NCCX_Sales_OrderLines که در کوئری فوق مورد استفاده قرار گرفته، همانطور که در قسمتی از تعریف آن نیز مشخص است، تعداد ستون‌های بیشتری را از آنچه ما نیاز داریم، در بر دارد. در این حالت آیا اگر ایندکس مناسب‌تری را با تعداد ستون کمتری ایجاد کنیم، از آن استفاده می‌کند؟
CREATE NONCLUSTERED INDEX [IX_OrderLines_StockItemID]
ON [Sales].[OrderLines](
[StockItemID] ASC,
[PickedQuantity] ASC,
[OrderID])
ON [PRIMARY];
GO
این ایندکس جدید، نیازهای واقعی کوئری نوشته شده را پوشش می‌دهد و تعداد ستون کمتری را به همراه دارد.
در این حالت اگر کوئری زیر را اجرا کنیم:
SELECT
    [ol].[OrderID],
    [ol].[OrderLineID],
    [ol].[StockItemID],
    [ol].[PickedQuantity],
    [si].[StockItemName],
    [si].[UnitPrice]
FROM [Sales].[OrderLines] [ol]
    JOIN [Warehouse].[StockItems] [si]
    ON [ol].[StockItemID] = [si].[StockItemID]
OPTION
(RECOMPILE);
GO
در کوئری پلن نهایی تفاوتی مشاهده نمی‌شود و باز هم SQL Server، همان COLUMNSTORE INDEX را به ایندکس جدید ترجیح داده‌است. علت اینجا است که ماهیت COLUMNSTORE INDEX‌ها فشرده شده‌است؛ در مقابل NONCLUSTERED INDEXها معمولی که به صورت پیش‌فرض غیر فشرده شده هستند و یک row store می‌باشند.

یک نکته: در این کوئری علت استفاده‌ی از RECOMPILE، وادار کردن SQL server به محاسبه‌ی مجدد کوئری پلن جاری است.

اکنون اگر نگارش فشرده شده‌ی ایندکسی را که ایجاد کردیم، با ذکر گزینه‌ی DATA_COMPRESSION = PAGE تعریف کنیم، چه اتفاقی رخ می‌دهد؟
CREATE NONCLUSTERED INDEX [IX_OrderLines_StockItemID_Compressed]
ON [Sales].[OrderLines](
[StockItemID] ASC,
[PickedQuantity] ASC,
[OrderID])
WITH (DATA_COMPRESSION = PAGE)
ON [PRIMARY];
GO
پس از آن مجددا همان کوئری قبلی را که به همراه RECOMPILE است، اجرا می‌کنیم. اینبار به کوئری پلنی خواهیم رسید که از این ایندکس جدید استفاده می‌کند.

یک نکته: اگر علاقمند بودید تا هزینه‌ی این کوئری‌ها را نسبت به یکدیگر محاسبه و مقایسه کنید، چون یک کوئری معمولی، همواره از آخرین پلن محاسبه شده استفاده می‌کند، اینکار میسر نیست. اما می‌توان با ذکر صریح ایندکس مدنظر توسط راهنمای WITH INDEX، بهینه ساز کوئری‌ها را وارد کرد تا از ایندکسی که ذکر می‌شود، بجای ایندکسی که فکر می‌کند بهتر است، استفاده کند. بنابراین اجرای هر 4 کوئری زیر با هم، 4 کوئری پلن متفاوت را بر اساس ایندکس‌های متفاوتی، محاسبه کرده و نمایش می‌دهد:
SELECT
    [ol].[OrderID],
    [ol].[OrderLineID],
    [ol].[StockItemID],
    [ol].[PickedQuantity],
    [si].[StockItemName],
    [si].[UnitPrice]
FROM [Sales].[OrderLines] [ol]
    JOIN [Warehouse].[StockItems] [si]
    ON [ol].[StockItemID] = [si].[StockItemID]
OPTION
(RECOMPILE);
GO

SELECT
    [ol].[OrderID],
    [ol].[OrderLineID],
    [ol].[StockItemID],
    [ol].[PickedQuantity],
    [si].[StockItemName],
    [si].[UnitPrice]
FROM [Sales].[OrderLines] [ol] WITH (INDEX (IX_Sales_OrderLines_Perf_20160301_02))
    JOIN [Warehouse].[StockItems] [si]
    ON [ol].[StockItemID] = [si].[StockItemID];
GO

SELECT
    [ol].[OrderID],
    [ol].[OrderLineID],
    [ol].[StockItemID],
    [ol].[PickedQuantity],
    [si].[StockItemName],
    [si].[UnitPrice]
FROM [Sales].[OrderLines] [ol] WITH (INDEX (IX_OrderLines_StockItemID))
    JOIN [Warehouse].[StockItems] [si]
    ON [ol].[StockItemID] = [si].[StockItemID];
GO

SELECT
    [ol].[OrderID],
    [ol].[OrderLineID],
    [ol].[StockItemID],
    [ol].[PickedQuantity],
    [si].[StockItemName],
    [si].[UnitPrice]
FROM [Sales].[OrderLines] [ol] WITH (INDEX (IX_OrderLines_StockItemID_Compressed))
    JOIN [Warehouse].[StockItems] [si]
    ON [ol].[StockItemID] = [si].[StockItemID];
GO


بررسی عملگر compute scalar

کار عملگر compute scalar، ارزیابی و محاسبه‌ی یک عبارت است و خروجی آن نیز یک مقدار scalar است؛ مانند functions در SQL Server. مشکلی که با این عملگر وجود دارد این است که هزینه‌ی انجام آن عموما در کوئری پلن ظاهر نمی‌شود (و یا با تخمین نادرستی ظاهر می‌شود) که می‌تواند گمراه کننده باشد. همچنین پلن حاصل، اشیایی را که توسط یک function مورد استفاده قرار می‌گیرند، لحاظ نمی‌کند.

برای نمونه اگر پلن دو کوئری زیر را با هم مقایسه کنیم:
SELECT COUNT(*)
FROM [Sales].[Orders];

SELECT COUNT_BIG (*)
FROM [Sales].[Orders];
تقریبا یکی هستند:


از این جهت که (*)COUNT در SQL server به (*)COUNT_BIG تفسیر شده و اجرا می‌شود. به همین جهت آنچنان تفاوتی در اینجا قابل مشاهده نیست.

اما اگر function زیر را تعریف کنیم:
CREATE FUNCTION dbo.CountProductsSold (
@SalesPersonID INT
) RETURNS INT

AS

BEGIN
    DECLARE @SoldCount INT;

    SELECT @SoldCount = COUNT(DISTINCT [ol].[StockItemID])
    FROM [Sales].[Orders] [o]
        JOIN [Sales].[OrderLines] [ol]
        ON [o].[OrderID] = [ol].[OrderID]
    WHERE [o].[SalespersonPersonID] = @SalesPersonID

    RETURN (@SoldCount);

END
و سپس پلن کوئری که از آن استفاده می‌کند را بررسی نمائیم:
SELECT
    [FullName] AS [SalesPerson],
    [dbo].[CountProductsSold]([PersonID]) AS [NumberOfProductsSold]
FROM [Application].[People]
WHERE [IsSalesperson] = 1;
مشاهده خواهیم کرد که در actual execution plan آن، هزینه‌ی فراخوانی این تابع صفر است و همچنین جزئیاتی از اشیایی که توسط آن فراخوانی شده‌اند نیز ذکر نشده‌است:


یک روش محاسبه‌ی هزینه‌ی فراخوانی این تابع، استفاده از extended events است. روش دیگر آن استفاده از اشیاء DMO's می‌باشد:
SELECT
    [fs].[last_execution_time],
    [fs].[execution_count],
    [fs].[total_logical_reads]/[fs].[execution_count] [AvgLogicalReads],
    [fs].[max_logical_reads],
    [t].[text],
    [p].[query_plan]
FROM sys.dm_exec_function_stats [fs]
CROSS APPLY sys.dm_exec_sql_text([fs].sql_handle) [t]
CROSS APPLY sys.dm_exec_query_plan([fs].[plan_handle]) [p];
این کوئری اطلاعات logical_reads مرتبط با تابع فراخوانی شده را گزارش می‌دهد که ... صفر نیست:


بنابراین compute scalar صورت گرفته دارای هزینه‌ای است که در actual execution plan ظاهر نمی‌شود.
اکنون اگر از منوی Query، گزینه‌ی Include actual execution plan را انتخاب نکنیم و بجای آن گزینه‌ی Display estimated execution plan را انتخاب کنیم، به تصویر زیر خواهیم رسید:


در نیمه‌ی پایینی آن، جزئیات دسترسی‌های تابع فراخوانی شده نیز ذکر می‌شوند. بنابراین استفاده‌ی از estimated execution planها در حین کار با توابع، بسیار مفید است.
مطالب
آشنایی با SQL Server Data Tools
مقدمه
یکی از امکانات اضافه شده در Sql Server 2012 ابزار Sql Server Data Tools یا به اختصار SSDT می‌باشد. این ابزار در واقع جایگزین ابزار Business Intelligence Development Studio می باشد که همه امکانات قبلی را داشته و همچنین قابلیت‌های جدیدی نیز به آن اضافه شد است. اما کاربرد این ابزار ایجاد محیطی یکپارچه برای طراحی و توسعه تمامی نسخه‌های پایگاه داده Sql Server با استفاده از Visual Studio می‌باشد. در این مقاله من به بیشتر بر روی امکانات زیر تمرکز دارم:
  • Installation
  • Creating a SQL Server Database Project
  • Modify Database Schema
  • Schema Compare and Update
  • Snapshot Project
  • Publish
  1. نصب Sql Server Data Tools
    آخرین نسخه این ابزار را از این آدرس دانلود کنید یا زمان نصب Sql Server 2012 گزینه آن را انتخاب کنید


  2. ایجاد یک پروژه جدید از نوع  SQL Server Database Project
    پس از نصب SSDT شما از طریق Visual Studio 2012 Shell که همراه SqlServer 2012 نصب می‌شود یا با Visual Stadio 2012 یک پروژه جدید از نوع SQL Server Database Project ایجاد کنید.

     Server Database Project Project به شما امکان توسعه پایگاه داده Sql Server را با استفاده  از محیط یکپارچه Visual Studio با در اختیار گذاشتن ابزار‌های همچون navigation, intellisense, validation, debugging, declarative editing و غیره را می‌دهد. شی پایگاه داده شما در پروژه و فایل‌های مجزا ذخیره می‌شود مثل آن که شما در حال توسعه برنامه #C یا VB.NET هستید.
  3. ورود پایگاه داده موجود به پروژه SQL Server Database
    شما می‌توانید شمای پایگاه داده موجود یا SQL Server DAC Package File (.dacpac) یا هر T-SQL دیگری را با راست کلیک بر روی پروژه و انتخاب گزینه Import به پروژه خود اضافه و تغییرات لازم را اعمال نماید شکل زیر پنجره Import Database  را نشان می‌دهد:
     

      پنجره 
    Solution Explorer امکانات زیر را در اختیار شما قرارمی دهد:

    - اضافه کردن اشیای همچون Table، View  و غیره با راست کیلک کردن بر روی پروژه و انتخاب گزینه Add
    - ویرایش اشیا موجود با دبل کیلک کردن بر روی اشیا
    - مقایسه شمای پایگاه داده با پایگاه داده دیگر و یا Microsoft SQL Server DAC Package File کافیست بر روی پروژه راست کیلک و گزینه Schema Compare را         انتخاب نمایید.
    - ایجاد یک Snapshot از شمای پایگاه داده در یک Microsoft SQL Server DAC Package File . یک snapshot برای ایجاد یک پایگاه داده یا ورود در پروژه ای دیگر یا در مقایسه دو پایگاه داده کاربرد دارد.
    - انتشار پایگاه داده که امکان ایجاد پایگاه داده یر روی یک سرور SQL Server را فراهم می‌نماید.  

  4. مقایسه شمای دو پایگاه داده با هم
    SSDT ابزاری برای مقایسه دو پایگاه داده و بروز رسانی پایگاه داده مقصد از روی شمای منبع از طریق ایجاد یک Script یا به صورت مستقیم را می‌دهد.



  5. انتشار پروژه با استفاده از SQL Server Data Tools
    این ابزار امکان گسترش پروژه شما بر روی SQL Server 2005, 2008, 2008 R2, 2012, یا SQL Azure instance را می‌دهد. برای انتشار کافیت از پنجره Solution Explorer  بر روی پروژه راست کیلک و گزینه Publish را انتخاب کنید.

  6. پنجره SQL Server Object Explorer
    این پنجره با اتصال به سرور SQL Server  امکان دسترسی و مدیریت پایگاه داده را به ما می‌دهد. علاوه بر دسترسی به اجزای یک پایگاه داده امکان مدیریت پوشه‌های Security , Server Objects نیز فراهم می‌باشد. همچنین امکان اتصال به Sql Server Express , localdb نیز وجود دارد. 


  7.   
مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت سوم - قالب پذیر ساختن گرید
در قسمت دوم، قالب نمایش ردیف‌های جدول، ثابت است و درون جدول به صورت مستقیمی درج و تعریف شده‌است. در ادامه می‌خواهیم این گرید را به نحوی تغییر دهیم که به ازای حالت‌های مختلفی مانند نمایش اطلاعات و یا ویرایش اطلاعات هر ردیف، از قالب‌های خاص آن‌ها استفاده شود.
قابلیتی که در ادامه از آن برای «قالب پذیر ساختن گرید» استفاده خواهیم کرد، همان نکته‌ی «امکان تعویض پویای قالب‌های یک دربرگیرنده» است که در مطلب «امکان تعریف قالب‌ها در Angular با دایرکتیو ng-template» به آن پرداختیم.


تعریف قالب‌های نمایش و ویرایش اطلاعات یک ردیف در گرید طراحی شده

پس از آشنایی با دایرکتیوهای تعریف و کار با قالب‌ها در Angular، اکنون تبدیل بدنه‌ی ثابت جدول، به دو قالب نمایش و ویرایش، ساده‌است.
در قسمت دوم این سری، کار رندر بدنه‌ی اصلی گرید توسط همین چند سطر، در قالب آن مدیریت می‌شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
        <td class="text-center">{{ item.productId }}</td>
        <td class="text-center">{{ item.productName }}</td>
        <td class="text-center">{{ item.price | number:'.0' }}</td>
        <td class="text-center">
          <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
            disabled="disabled" />
        </td>
      </tr>
    </tbody>
  </table>

در ادامه قسمت داخلی ngFor را تبدیل به یک ng-container می‌کنیم تا قالب پذیر شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <ng-container [ngTemplateOutlet]="loadTemplate(item)"
                 [ngOutletContext]="{ $implicit: item, idx: i }"></ng-container>
      </tr>
    </tbody>
کار دایرکتیو ngOutletContext، تنظیم شیء context هر قالب است. به این ترتیب شیء متناظر با هر ردیف و همچنین ایندکس آن‌را به هر قالب ارجاع می‌دهیم. خاصیت implicit$ به این معنا است که اگر منبع داده‌ی متغیر ورودی مشخص نشد، از مقدار item استفاده شود.
در اینجا ngTemplateOutlet این امکان را می‌دهد تا بتوان توسط کدهای برنامه، قالب هر ردیف را مشخص کرد. متد loadTemplate در کدهای کامپوننت متناظر فراخوانی شده و بر اساس وضعیت هر ردیف، یکی از دو قالب ذیل را بازگشت می‌دهد:

الف) قالب نمایش معمولی و فقط خواندنی رکوردها


<!--The Html Template for Read-Only Rows-->
<ng-template #readOnlyTemplate let-item let-i="idx">
  <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
  <td class="text-center">{{ item.productId }}</td>
  <td class="text-center">{{ item.productName }}</td>
  <td class="text-center">{{ item.price | number:'.0' }}</td>
  <td class="text-center">
    <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
      disabled="disabled" />
  </td>
  <td>
    <input type="button" value="Edit" class="btn btn-default btn-xs" (click)="editItem(item)"
    />
  </td>
  <td>
    <input type="button" value="Delete" (click)="deleteItem(item)" class="btn btn-danger btn-xs"
    />
  </td>
</ng-template>
همانطور که ملاحظه می‌کنید، در اینجا بدنه‌ی ngFor را به یک ng-template مشخص شده‌ی با readOnlyTemplate# انتقال داده‌ایم. همچنین دو متغیر ورودی item و i را توسط -let تعریف کرده‌ایم. چون عبارت منبع داده item مشخص نشده‌است، از همان خاصیت implicit$ شیء context استفاده می‌کند.
این قالب در کدهای کامپوننت آن به صورت ذیل قابل دسترسی و انتخاب شده‌است:
 @ViewChild("readOnlyTemplate") readOnlyTemplate: TemplateRef<any>;

ب) قالب ویرایش اطلاعات هر ردیف که از آن برای افزودن یک ردیف جدید هم می‌توان استفاده کرد



شبیه به همان کاری را که برای نمایش ردیف‌های فقط خواندنی انجام دادیم، در مورد قالب ویرایش هر ردیف نیز تکرار می‌کنیم. در اینجا فقط امکان ویرایش نام محصول، قیمت آن و موجود بودن آن‌را توسط یک‌سری input box مهیا کرده‌ایم:
<!--The Html Template for Editable Rows-->
<ng-template #editTemplate let-item let-i="idx">
  <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
  <td class="text-center">{{ item.productId }}</td>
  <td class="text-center">
    <input type="text" [(ngModel)]="selectedItem.productName" class="form-control" />
  </td>
  <td class="text-center">
    <input type="text" [(ngModel)]="selectedItem.price" class="form-control" />
  </td>
  <td class="text-center">
    <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
      [(ngModel)]="selectedItem.isAvailable" />
  </td>
  <td>
    <input type="button" value="Save" (click)="saveItem()" class="btn btn-success btn-xs"
    />
  </td>
  <td>
    <input type="button" value="Cancel" (click)="cancel()" class="btn btn-warning btn-xs"
    />
  </td>
</ng-template>
به این قالب نیز با توجه به template reference variable آن که editTemplate# نام دارد، به صورت ذیل در کامپوننت متناظر دسترسی خواهیم یافت.
 @ViewChild("editTemplate") editTemplate: TemplateRef<any>;

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


خواص عمومی مورد نیاز جهت کار با قالب‌ها و ویرایش‌های درون ردیفی

@ViewChild("readOnlyTemplate") readOnlyTemplate: TemplateRef<any>;
@ViewChild("editTemplate") editTemplate: TemplateRef<any>;
selectedItem: AppProduct;
isNewRecord: boolean;
برای اینکه بتوانیم قالب‌ها را به صورت پویا تعویض کنیم، نیاز است در کدهای کامپوننت، به آن‌ها دسترسی داشت. اینکار را توسط تعریف ViewChildهایی با همان نام template reference variable قالب‌ها انجام داده‌ایم.
به علاوه اگر به قالب editTemplate دقت کنید، مقدار ویرایش شده به [(ngModel)]="selectedItem.productName" انتساب داده می‌شود. به همین جهت شیء selectedItem نیز تعریف شده‌است.
همچنین نیاز است بدانیم اکنون در حال ویرایش یک ردیف هستیم یا این ردیف، کاملا ردیف جدیدی است. به همین جهت پرچم isNewRecord نیز تعریف شده‌است.


فعالسازی قالب ویرایش هر ردیف

در انتهای هر ردیف، دکمه‌ی ویرایش نیز قرار دارد که به (click) آن، رخداد editItem متصل است:
editItem(item: AppProduct) {
  this.selectedItem = item;
}
در اینجا Item انتخابی را به selectedItem انتساب می‌دهیم. همین مساله سبب محاسبه‌ی مجدد ردیف می‌شود. یعنی متد loadTemplate داخل حلقه‌ی ngFor مجددا فراخوانی می‌شود:
  loadTemplate(item: AppProduct) {
    if (this.selectedItem && this.selectedItem.productId === item.productId) {
      return this.editTemplate;
    } else {
      return this.readOnlyTemplate;
    }
  }
در اینجا بررسی می‌کنیم که آیا در حال ویرایش اطلاعات هستیم؟ آیا selectedItem  مقدار دهی شده‌است و نال نیست؟ اگر بله، قالب editTemplate را بازگشت می‌دهیم. اگر خیر، قالب نمایش ردیف‌های فقط خواندنی بازگشت داده می‌شود. به این ترتیب می‌توان در کدهای برنامه به صورت پویا، در مورد نمایش قالبی خاص تصمیم‌گیری کرد.


مدیریت افزودن یک ردیف جدید

دکمه‌ی افزودن یک ردیف جدید به صورت ذیل به قالب اضافه شده‌است:
<div class="panel">
  <input type="button" value="Add new product" class="btn btn-primary" (click)="addItem()"
  />
</div>
بنابراین نیاز است رخ‌داد addItem آن‌را به صورت ذیل تعریف کرد:
  addItem() {
    this.selectedItem = new AppProduct(0, "", 0, false);
    this.isNewRecord = true;

    this.queryResult.items.push(this.selectedItem);
    this.queryResult.totalItems++;
  }
در اینجا برخلاف حالت ویرایش که selectedItem را به item انتخابی ردیف جاری تنظیم کردیم، آن‌را به یک شیء جدید و تازه تنظیم می‌کنیم. همچنین پرچم isNewRecord  را نیز true خواهیم کرد. سپس این آیتم را به لیست رکوردهای موجود گرید نیز اضافه می‌کنیم. همینقدر تغییر، سبب محاسبه‌ی مجدد loadTemplate و بارگذاری قالب ویرایشی آن می‌شود.


مدیریت لغو ویرایش هر ردیف

برای اینکه ویرایش هر ردیف را لغو کنیم و قالب آن‌‌را به حالت فقط خواندنی بازگشت دهیم، فقط کافی است selectedItem را به نال تنظیم کنیم:
cancel() {
  this.selectedItem = null;
}
با این تنظیم و محاسبه‌ی خودکار و مجدد متد loadTemplate، قسمت return this.readOnlyTemplate فعال می‌شود که سبب نمایش عادی یک ردیف خواهد شد.


مدیریت حذف هر ردیف

در اینجا با پیاده سازی متد رخ‌دادگردان deleteItem و ارسال id هر ردیف به سرور، کار حذف هر ردیف را انجام خواهیم داد:
  deleteItem(item: AppProduct) {
    this.productsService
      .deleteAppProduct(item.productId)
      .subscribe((resp: Response) => {
        this.getPagedProductsList();
      });
  }


مدیریت ثبت و یا به روز رسانی هر ردیف

آخرین عملیاتی که باید مدیریت شود، بررسی پرچم isNewRecord است. اگر true بود، کار افزودن یک ردیف جدید صورت گرفته و سپس این پرچم false می‌شود. اگر false بود، به معنای درخواست به روز رسانی ردیفی مشخص است. در پایان هر دو عملیات selectedItem را نیز true می‌کنیم و این پایان عملیات باید داخل قسمت دریافت پاسخ از سرور مدیریت شود و نه پس از فراخوانی این متدها؛ چون متدهای subscribe غیرهمزمان بوده و ردیف‌های پس از آن‌ها بلافاصله اجرا می‌شوند.
  saveItem() {
    if (this.isNewRecord) {
      this.productsService
        .addAppProduct(this.selectedItem)
        .subscribe((resp: AppProduct) => {
          this.selectedItem.productId = resp.productId;
          this.isNewRecord = false;
          this.selectedItem = null;
        });
    } else {
      this.productsService
        .updateAppProduct(this.selectedItem.productId, this.selectedItem)
        .subscribe((resp: AppProduct) => {
          this.selectedItem = null;
        });
    }
  }


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
اجرای وظایف زمان بندی شده با Quartz.NET - قسمت اول
مقدمه
اگر  قصد اجرای برخی کارها به صورت زمانبندی شده و در فواصل زمانی مشخص را دارید، این مقاله به شما کمک خواهد کرد تا به بهترین شکل ممکن آن را انجام دهید. کارهایی مانند ارسال خبرنامه، فرستادن SMS تبریک تولد یا هماهنگ سازی داده‌ها بین دو منبع داده از جمله اَعمالی هستند که باید به صورت زمانبندی شده انجام شوند.
کتابخانه‌ی Quartz.NET، از کتابخانه ای با نام Quartz و از زبان Java به NET. منتقل شده است. Quartz.NET، رایگان و باز متن است و از طریق آدرس http://quartznet.sourceforge.net در دسترس است. از طریق NuGet نیز می‌توانید با تایپ عبارت quartz در فرم مربوطه، این کتابخانه را نصب کنید. این کتابخانه را در برنامه‌های Desktop و Web (حتی یک Shared Server) تست کردم و به خوبی انجام وظیفه می‌کند.

شروع کار با Quartz.NET
ضمن در اختیار قرار دادن امکانات فوق العاده و انعطاف پذیری بسیار، کار با این کتابخانه آسان و از فرایندی منطقی تبعیت می‌کند. فرایند اجرای یک روال زمانبندی شده از طریق Quartz.NET، از چهار مرحله‌ی اصلی تشکیل شده است.
1) پیاده سازی اینترفیس IJob
2) مشخص کردن جزئیات روال با اینترفیس IJobDetail
3) مشخص کردن تنظیمات زمان با استفاده از اینترفیس ITrigger
4) مدیریت اجرا با استفاده از اینترفیس IScheduler

مثالی را بررسی می‌کنیم. در این مثال قصد داریم تا عبارتی را همراه با تاریخ و زمان جاری در یک فایل ذخیره کنیم. این پیغام باید 3 بار و در فواصل زمانی 10 ثانیه به فایل اضافه شود. در پایان، فایلی خواهیم داشت که در سه خط، یک عبارت، همراه با تاریخ و زمان‌های مختلف را که 10 ثانیه با یکدیگر اختلاف دارند در خود ذخیره کرده است. ابتدا کار زمانبندی شده را با ارائه‌ی پیاده سازی برای متد Execute اینترفیس IJob این کتابخانه ایجاد می‌کنیم. وارد کردن فضای نام Quartz را فراموش نکنید.
namespace SchedulerDemo.Jobs
{
    using System;
    using System.IO;
    using Quartz;

    public class HelloJob : IJob
    {
        public void Execute(IJobExecutionContext context)
        {
            // for web apps
            // string path = System.Web.Hosting.HostingEnvironment.MapPath("~/Data/Log.txt");
            
            // for desktop apps
            string path = @"C:\Log.txt";

            using (StreamWriter sw = new StreamWriter(path, true))
            {
                sw.WriteLine("Message from HelloJob " + DateTime.Now.ToString());
            }
        }
    }
}
در اینترفیس IJob در ASP.NET، به شی HttpContext دسترسی ندارید، بنابراین در صورتی که قصد داشته باشید از متدی مانند Server.MapPath استفاده کنید، توفیقی به دست نخواهید آورد. در عوض می‌توانید از متد System.Web.Hosting.HostingEnvironment.MapPath استفاده کنید.
حال، زمان انجام تنظیمات مختلف برای اجرای روال مربوطه است. بهتر است تا interfaceیی ایجاد و متدی با نام Run در آن داشته باشیم.
namespace SchedulerDemo.Interfaces
{
    public interface ISchedule
    {
        void Run();
    }
}

حال، پیاده سازی خود را برای این interface ارائه می‌دهیم.

namespace SchedulerDemo.Jobs
{
    using System;
    using Quartz;
    using Quartz.Impl;
    using SchedulerDemo.Interfaces;
    using SchedulerDemo.Jobs;

    public class HelloSchedule : ISchedule
    {
        public void Run()
        {
            //DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 2);
            DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second);

            IJobDetail job = JobBuilder.Create<HelloJob>()
                                       .WithIdentity("job1")
                                       .Build();

            ITrigger trigger = TriggerBuilder.Create()
                                             .WithIdentity("trigger1")
                                             .StartAt(startTime)
                                             .WithSimpleSchedule(x => x.WithIntervalInSeconds(10).WithRepeatCount(2))
                                             .Build();

            ISchedulerFactory sf = new StdSchedulerFactory();
            IScheduler sc = sf.GetScheduler();
            sc.ScheduleJob(job, trigger);

            sc.Start();
        }
    }
}

معرفی فضاهای نام Quartz و Quartz.Impl را فراموش نکنید.
از حالا، به روالی که قرار است به صورت زمانبندی شده اجرا شود، "وظیفه" می‌گوییم.
ابتدا باید مشخص کنیم که وظیفه در چه زمانی پس از اجرای برنامه شروع به اجرا کند. از آنجا که پایه و اساس زمانبندی، بر تاریخ و ساعت استوار است، کتابخانه‌ی Quartz.NET، روش‌ها و امکانات بسیاری را برای تعیین زمان در اختیار قرار می‌دهد. با بررسی تمامی آنها، ساده‌ترین و منعطف‌ترین را به شما معرفی می‌کنم. کلاس DateBuilder که همراه با Quartz.NET وجود دارد، امکان تعیین زمان را به اَشکال مختلف می‌دهد. در خط 14، از متد FutureDate این کلاس استفاده شده است که خوانایی بهتری نسبت به بقیه‌ی متدها دارد. پارامتر اول این متد، عدد، و پارامتر دوم، واحد زمانی را می‌پذیرد.

DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second);

در اینجا، زمان آغاز وظیفه را 2 ثانیه پس از آغاز برنامه تعریف کرده ایم. واحدهای زمانی دیگر شامل میلی ثانیه، دقیقه، ساعت، روز، ماه، هفته و سال هستند. کلاس DateBuilder، متدهای مختلفی برای تعیین زمان را در اختیار قرار می‌دهد. تعیین زمان آغاز به روش دیگر را به صورت کامنت شده در خط 13 مشاهده می‌کنید.
وظیفه‌ی ایجاد شده در خط 16 تا 18 معرفی شده است. 

IJobDetail job = JobBuilder.Create<HelloJob>()
                           .WithIdentity("job1")
                           .Build();

پشتیبانی Quartz.NET از سینتکس fluent، کدنویسی را ساده و لذت بخش می‌کند. با استفاده از متد Create کلاس JobBuilder، وظیفه را معرفی می‌کنیم. متد Create، یک متد Generic است که نام کلاسی که اینترفیس IJob را پیاده سازی کرده است می‌پذیرد. یک نام را با استفاده از متد WithIdentity به وظیفه نسبت می‌دهیم (البته این کار، اختیاری است) و در انتها، متد Build را فراخوانی می‌کنیم. خروجی متد Build، از نوع IJobDetail است.
و حالا نوبت به تنظیمات زمان رسیده است. در Quartz.NET، این مرحله، "ایجاد trigger" نام دارد. خطوط 20 تا 24 به این کار اختصاص دارند. 

ITrigger trigger = TriggerBuilder.Create()
                                 .WithIdentity("trigger1")
                                 .StartAt(startTime)
                                 .WithSimpleSchedule(x => x.WithIntervalInSeconds(10).WithRepeatCount(2))
                                 .Build();

ابتدا متد Create کلاس TriggerBuilder را فراخوانی می‌کنیم، سپس با استفاده از متد WithIdentity، یک نام به trigger اختصاص می‌دهیم (البته این کار، اختیاری است). با متد StartAt، زمان شروع وظیفه را که در ابتدا با استفاده از کلاس DateBuilder ایجاد کردیم تعیین می‌کنیم. مهمترین قسمت، تعیین دفعات و فواصل زمانی اجرای وظیفه است. همان طور که احتمالاً حدس زده اید، Quartz.NET مجموعه ای غنی از روش‌های مختلف برای تعیین بازه‌ی زمانی اجرا را در اختیار قرار می‌دهد. آسان‌ترین راه، استفاده از متد WithSimpleSchedule است. با استفاده از یک عبارت Lambda که ورودی آن از نوع کلاس SimpleScheduleBuilder است، دفعات و فواصل زمانی اجرا را تعیین می‌کنیم. متد WithIntervalInSeconds، برای تعیین فواصل زمانی در بازه‌ی ثانیه استفاده می‌شود. متد WithRepeatCount نیز برای تعیین دفعات اجرا است. وظیفه‌ی ما، 3 مرتبه و در فواصل زمانی 10 ثانیه اجرا می‌شود. مطمئن باشید اشتباه نکردم! بله، سه مرتبه. تعداد دفعات اجرا برابر است با عددی که برای متد WithRepeatCount تعیین می‌کنید، به علاوه‌ی یک. منطقی است، چون مرتبه‌ی اول اجرا زمانی است که با استفاده از متد StartAt تعیین کرده اید. در پایان، متد Build را فراخوانی می‌کنیم. خروجی متد Build، از نوع ITrigger است.
آخرین کار (خطوط 26 تا 30)، ایجاد شی از اینترفیس IScheduler، فراخوانی متد ScheduleJob آن، و پاس دادن اشیای job و trigger که در قسمت قبل ایجاد شده اند به این متد است. در انتها، متد ()Start را برای آغاز وظیفه فراخوانی می‌کنیم. 

ISchedulerFactory sf = new StdSchedulerFactory();
IScheduler sc = sf.GetScheduler();
sc.ScheduleJob(job, trigger);

sc.Start();

حال شما یک وظیفه تعریف کرده اید که در هر جای برنامه به صورت زیر، قابل فراخوانی است. 

ISchedule myTask = new HelloSchedule();
myTask.Run();

کتابخانه ای که با آن سر و کار داریم بسیار غنی است و امکانات بسیاری دارد. در قسمت بعد، با برخی امکانات دیگر این کتابخانه آشنا می‌شوید.

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


کارهای لازم پیش از طرح سؤال
- سعی کنید انجمن‌های مرتبط را یکبار بررسی و جستجو کنید.
- عین خطای دریافتی را در گوگل جستجو کنید. اگر از برنامه‌ها یا کتابخانه‌های معروف و متداول استفاده می‌کنید، یکی از مزیت‌های مهم کار با آن‌ها، «تنها نبودن» است! یقین داشته باشید خطایی را که دریافت کرده‌اید پیشتر توسط ده‌ها نفر دیگر در سایت‌های مختلف مطرح شده‌اند و بالاخره با بررسی آن‌ها می‌توان به پاسخ رسید.
- شاید راهنمای برنامه در این مورد خاص مطلبی را عنوان کرده است.

و ... به صورت خلاصه باید بتوانید به این سؤال پاسخ دهید: «خودت چکار کردی؟». حداقل نشان دهید که فرد حاضر و آماده طلبی نیستید و پیشتر یک حداقل تقلایی را انجام داده‌اید.


کجا باید سؤال پرسید؟
- اگر به انجمنی برای طرح سؤال خود مراجعه کرده‌اید، حتما زیر شاخه صحیحی را انتخاب کنید تا سؤال شما بسته نشود یا کلا حذف نگردد. برای مثال سؤال ASP.NET را در بخش سی‌شارپ نپرسید یا برعکس یا اگر سایتی مقاله‌ای را منتشر کرده، ذیل آن در مورد نحوه بک آپ گرفتن از اکانت توئیتر خود سؤال نپرسید!
- اگر پاسخی را دریافت کردید، ادامه بحث را ذیل همان مطلب پیگیری کنید و مجددا مطلب جدیدی را ایجاد نکنید.
- اگر تا نیم ساعت بعد جوابی را دریافت نکردید، کل بخش‌های یک سایت را با ارسال پیام خود اسپم نکنید. یکبار ارسال یک سؤال کافی است. اکثر این سایت‌ها حالت یک «چت آفلاین» را دارند. به این معنا که ابتدا پیغام خود را می‌گذارید، اگر مدتی بعد (ممکن است چند ساعت بعد) شخصی آن‌را مشاهده کرد و قادر به پاسخ دهی بود، به شما کمک خواهد کرد. بنابراین اگر سریعا به جواب نرسیدید، نه کل سایت را اسپم کنید و نه ... شروع به رفتارهای ناشایست کنید. اینکار با فریاد کشیدن وسط یک جمع تفاوتی ندارد. اشخاص مرتبط همواره آنلاین نیستند؛ ضمنا ممکن است واقعا پاسخی برای یک سؤال نداشته باشند. منصف باشید.
- از ایمیل‌های خصوصی افراد یا قسمت پیام‌های خصوصی سایت‌ها برای ارسال سؤالات شخصی استفاده نکنید. ایمیل خصوصی، مخصوص کارهای شخصی است. قسمت پیام‌های خصوصی یک سایت عموما مخصوص رسیدگی به مشکلات کاربری است. این تصور را نداشته باشید که اشخاص مشاور شخصی رایگان پروژه‌های تجاری شما هستند.
- بهترین محل برای پرسیدن سؤالات مرتبط با یک پروژه خاص، mailing list یا انجمن گفتگو و یا issue tracker آن پروژه است. وقت خودتان را با ارسال خطاهای یک پروژه خاص، در یک انجمن عمومی و همه منظوره تلف نکنید. کمی جستجو کنید که سایت اصلی پروژه کجا است. بعد دقت کنید آیا جایی برای پرسش و پاسخ دارد یا خیر. اکثر پروژه‌های خوب، مکانی را جهت جمع آوری بازخوردهای پروژه خود، اختصاص می‌دهند.


چطور باید سؤال پرسید؟
سؤال فنی خوب پرسیدن هم یک هنر است؛ که تعدادی از مشخصه‌های مهم آن‌را در ذیل مرور خواهیم کرد:
- عنوان مناسبی را برای سؤال خود انتخاب کنید. «لطفا کمک کنید» یا «من مشکل دارم» یا «مشکل در پروژه»، عموما واکنش‌های تندی را به همراه دارند؛ و تا حد ارسال اسپم در یک سایت بی‌کیفیت تلقی می‌شوند. ضمن اینکه انتخاب عنوان‌های مناسب، جستجوهای بعدی را در سایت ساده می‌کنند و کمک بزرگی خواهند بود به افراد بعدی.
- محیطی را که خطا در آن رخ داده است، توضیح دهید. ذکر IIS تنها کافی نیست. کدام نگارش آن؟ در کدام ویندوز؟
برای مثال شماره نگارش کتابخانه یا نرم افزار مورد استفاده را ذکر کنید. شاید خطایی که گرفته‌اید در نگارش بعدی آن برطرف شده است.
ذکر شماره نگارش VS.NET یا شماره نگارش دات نت مورد استفاده، سیستم عامل و کلا توصیف محیط بروز خطا، عموما بسیار مفید هستند.
- حتما کل خطای دریافت شده را ارسال کنید. اگر در یک برنامه C خطایی حاصل شود، احتمالا شکلی مانند Error 0xABCD را دارد. اما استثناءهای دات نت به همراه stack trace و حتی شماره سطر خطای حاصل نیز هستند. همین مساله می‌تواند به خطایابی نهایی بسیار کمک کند.
- سؤال خود را طوری مطرح کنید که شخص مقابل بتواند آن‌را در کمترین زمان ممکن «باز تولید» کند. برای مثال ذکر خطای دریافتی بسیار خوب است. اگر داده‌ای که سبب بروز این خطا شده است را هم ارسال کنید، مفید‌تر خواهد بود؛ یا اگر دستور پاور شل خاصی در کنسول نیوگت خطا می‌دهد، صرفا عنوان نکنید که جواب نگرفته‌اید. چه دستوری را اجرا کرده‌اید؟ چه خطایی را دریافت کرده‌اید؟ ساختار پروژه شما چیست؟ آیا شخص مقابل می‌تواند بر اساس اطلاعاتی که ارائه دادید یک آزمایش شخصی را تدارک ببیند؟ آیا می‌تواند آن‌را با توضیحات شما مجددا تولید کند؟
زمان باز تولید خطا را هم مدنظر داشته باشید. برای مثال اگر بتوانید قطعه کدی را ارائه دهید که در کمترین زمان ممکن، صرفا با کپی و پیست آن در VS.NET قابل کامپایل باشد، بسیاری علاقمند به پاسخگویی به شما خواهند شد. در غیراینصورت آنچنان انتظار نداشته باشید که شخص پاسخ دهنده وقت زیادی را برای رسیدگی به جزئیات سؤال شما صرف کند؛ یا مدتی مشغول به تهیه یک مثال جدید بر مبنای توضیحات شما شود.
حجم کدهای ارسالی شما نیز در اینجا مهم هستند. کل پروژه خود را ارسال نکنید! سعی کنید یک مثال کوچک را که بتواند سریعا خطای مدنظر شما را بازتولید کند، ارسال کنید و نه بیشتر. همچنین کدهایی که برای اجرا نیاز به GUI نداشته باشند نیز در این حالت اولویت دارند.
و به صورت خلاصه، خودتان را بجای پاسخ دهنده قرار دهید. آیا با چند جمله‌ای که ارائه داده‌اید، می‌توان انتظار پاسخی را داشت یا خیر.
- ایمیل شخصی خود را در انتهای پیام ارسال نکنید. کسی اهمیتی نمی‌دهد! اگر سؤال شما پاسخی داشته باشد، همانجا دریافت خواهید کرد و نه در میل باکس شخصی.
- املاء و انشای متنی را که ارسال می‌کنید، یکبار بررسی کنید. اگر برای شما اهمیتی ندارد که چه کلمات و جمله بندی را باید بکار برد، برای شخص مقابل هم آنچنان اهمیتی نخواهد داشت که زیاد وقت صرف کند.
- از بکار بردن smileyهای بیش از حد یا قرار دادن تعداد علامت تعجب‌های بیش از حد خودداری کنید. این موارد عموما به مسخره کردن شخص مقابل تفسیر می‌شوند.
- در بدو امر فریاد نکشید که «باگ» پیدا کرده‌اید؛ خصوصا اگر به mailing list اختصاصی یک پروژه پیامی را ارسال می‌کنید. چون اگر مشکل شما واقعا باگ نباشد، بیشتر یک توهین تلقی خواهد شد و در دفعات بعدی پاسخ دادن به شما به صورت ضمنی مؤثر خواهند بود؛ یا جواب نمی‌گیرید و یا جدی گرفته نخواهید شد.  
- هدف از کاری را که مشغول به انجام آن بود‌ه‌اید را نیز ذکر کنید. ذکر خطای دریافتی بسیار مفید است اما اگر بتوانید یک دید کلی را نسبت به کاری که مشغول به آن بوده‌اید، ایجاد کنید، شاید پاسخ بهتری را دریافت کنید. برای مثال جهت رسیدن به هدف و مقصود شما بهتر است از روش دیگری استفاده کنید.
- پس از اینکه پیامی را دریافت کردید، یک حداقل واکنشی را ارسال کنید. مثلا خوب بود؛ کمک کرد و یا مفید نبود. همین واکنش‌ها در آینده به کمک نتایج جستجوهای انجام شده خواهند آمد و اشخاص بعدی حداقل خواهند دانست که پاسخ داده شده صحیح بوده است یا خیر.

و همیشه بخاطر داشته باشید: تمام خدماتی که سایت‌های عمومی به شما ارائه می‌دهند «یک لطف» است و حقی را برای شما ایجاد نمی‌کنند. این اشخاص از شما پول نمی‌گیرند تا به سؤالات شما پاسخ دهند یا تبدیل به مشاور خصوصی رایگان شما شوند. می‌توانید محیط را برای این اشخاص، با اندکی احترام، ملایمت و انصاف، دلپذیرتر کنید.