نظرات مطالب
شروع به کار با DNTFrameworkCore - قسمت 2 - طراحی موجودیت‌های سیستم
در روابط One to One و One to Zero جهت پیاده سازی مباحث ردیابی تغییرات، ارث بری از TrackableEntity باعث ایجاد یک PrimaryKey جدید میشود. درصورتیکه کلید خارجی و کلید اصلی در جدول دوم بایستی یکی باشند. راه حلی در این مورد دارید؟
نظرات مطالب
Asp.Net Identity #2
چون به همین صورت جنریک تعریف شده و همچنین دارای امضاهای متفاوتی هم هست (جهت تعیین نوع کلید اصلی که حالت پیش فرض آن string است). در مطلب فوق تعریف AppUser : IdentityUser قابل مشاهده‌است. اگر نیاز به تعویض این کلید اصلی پیش فرض را داشتید مطلب «اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity» را دنبال کنید.
نظرات مطالب
سفارشی کردن ASP.NET Identity در MVC 5

سلام؛ از زحماتتون بسیار تشکر می‌کنم. من وب سایت را به روش چند لایه‌ی مرسوم می‌سازم:

data layer - domain models - website - service layer

با توجه به آن چگونه می‌توان از asp.net identity استفاده کرد؟ زیرا مثلا نیاز است از کلید جدول users در مدل‌های دیگه استفاده شود. آیا این کلید را باید در لایه دومین استفاده کرد؟

نظرات مطالب
EF Code First #7

سعید جان میدونم که اینکار میشه- مطمئنا شماره ملیشون رو  uniqe کردم!

ما برای انتخاب کلید اصلی دو حالت داریم -

1- استفاده از کلیدهای طبیعی  مثل شماره پرسنلی

2- استفاده از کلیدهای جانشین مثل یک فیلد identity - این حالت موقعی استفاده میشه که کلید طبیعی نداشته باشیم

مطالب
بررسی الگوهای ایندکس‌های Non-Clustered در SQL Server

قصد داریم الگوهای مختلف ایندکس گذاری و استراتژی Non-Clustered Indexes را در Sql Server، بررسی کنیم.

مزایای ایجاد ایندکس‌های صحیح بر اساس نیازهای واقعی کاری:

  • سریعتر شدن اجرای کوئری‌های جستجو در تعداد رکوردهای بالا
  • مرتب سازی سریعتر نتایج (sorting)
  • کوئری‌هایی که بر اساس عبارت GROUP BY ایجاد شده‌اند، سریعتر اجرا خواهند شد 

Non-Clustered Indexes 

تقریبا در تمام دیتابیس‌ها به راه‌های دیگری برای دسترسی به داده‌های جداول نیاز خواهد شد که لزوما این داده‌ها براساس ترتیب هنگام ذخیره سازی، مرتب نیستند. در چنین شرایطی ایندکس‌های غیر خوشه‌ای بر سر کار خواهند آمد.
در ادامه الگوهای مختلف ایندکس گذاری مرتبط با ایندکس‌های غیر خوشه‌ای را بررسی کرده و برای هر کدام از آنها مثالی را بررسی خواهیم کرد. خواهیم دید هر ایندکسی که از جانب ما ایجاد می‌شود، نمیتوان مطمئن شد که توسط Sql Server  مورد استفاده قرار می‌گیرد!
این الگو‌ها در تعیین زمان و مکان ساخت ایندکس‌های غیر خوشه‌ای، به ما کمک خواهند کرد که به شرح زیر می‌باشند:
  • Search Columns
  • Index Intersection
  • Multiple Columns
  • Covering Indexes
  • Included Columns
  • Filterd Indexes
  • Foreign Keys

Search Columns

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

USE AdventureWorks2012;

GO
CREATE TABLE dbo.Contacts (
    ContactID         INT           IDENTITY (1, 1),
    FirstName         NVARCHAR (50),
    LastName          NVARCHAR (50),
    IsActive          BIT          ,
    EmailAddress      NVARCHAR (50),
    CertificationDate DATETIME     ,
    FillerData        CHAR (1000)  ,
    CONSTRAINT PK_Contacts PRIMARY KEY CLUSTERED (ContactID)
);

INSERT INTO dbo.Contacts (FirstName, LastName, IsActive, EmailAddress, CertificationDate)
SELECT pp.FirstName,
       pp.LastName,
       IIF (pp.BusinessEntityID / 10 = 1, 1, 0),
       pea.EmailAddress,
       IIF (pp.BusinessEntityID / 10 = 1, pp.ModifiedDate, NULL)
FROM   Person.Person AS pp
       INNER JOIN
       Person.EmailAddress AS pea
       ON pp.BusinessEntityID = pea.BusinessEntityID;

ابتدا قصد داریم از جدول Contacts بدون استفاده از هیچ ایندکس غیر خوشه‌ای، کوئری بگیریم. نتیجه‌های نشان داده شده‌ی در کوئری حاصل از کد T-SQL زیر به شرح زیر است:

SET STATISTICS IO ON;

SELECT ContactID,
       FirstName
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine';

SET STATISTICS IO OFF;

22 رکورد را واکشی کرده است؛ ولی با خواندن 2866 page ! که این تعداد، تمام صفحات موجود در جدول می‌باشد. بنابراین واکشی این تعداد رکورد از کل رکورد‌های موجود در جدول (19000) نیاز به چک کردن همه‌ی صفحات را خواهد داشت که واقعا روش بهینه‌ای نمی‌باشد. 

همانطور که در تصویر پلن کوئری بالا هم مشخص است، کل ایندکس خوشه دار ما Scan شده است که هزینه‌ی بالایی خواهد داشت.

حال با کد T-SQL زیر یک ایندکس غیر خوشه دار را بر روی فیلد FirstName ایجاد خواهیم کرد:

CREATE INDEX IX_Contacts_FirstName ON dbo.Contacts(FirstName);

اگر دوباره کوئری قبلی را اجرا کنیم، به نتایج خیلی بهتری خواهیم رسید و تعداد صفحات خوانده شده به 2 کاهش یافته است! 

Sql Server این بار به جای اسکن دوباره‌ی ایندکس خوشه دار، با استفاده از Index Seek و بهره بردن از ایندکس ایجاد شده‌ی توسط ما، یک پلن قابل قبول را برای ما ارائه داده است.

Index Intersection

در برخی از سناریوها لازم است یکسری ستون دیگر هم علاوه بر ستونی که ایندکس را بر روی آن تعریف کرده‌ایم، در بخش شرط یا خروجی select استفاده شوند. یکی از راه‌حل‌ها، ایجاد یک ایندکس غیر خوشه‌ای که سایر ستون‌ها را نیز Include می‌کند، می‌باشد. با وجود ایندکس‌هایی که هر کدام از آنها می‌توانند برای ادا کردن بخشی از شروط، نقش ایفا کنند، Sql Server  هم با به کار بردن آنها می‌تواند رکوردهایی که در فصل مشترک حاصل از جسجتوی این ایندکس‌ها بدست آمده را به عنوان خروجی کوئری ما بازگشت دهد. این عملیات Index Intersection نام دارد. به مثال زیر توجه کنید:

SET STATISTICS IO ON;

SELECT ContactID,
       FirstName,
       LastName
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine'
       AND LastName = 'Cox';

SET STATISTICS IO OFF;

در کوئری بالا علاوه بر FirstName که یک ایندکس غیر خوشه دار را بر روی آن ایجاد کرده‌ایم، فیلد LastName را هم در بخش Select و شرط، مطرح کرده‌ایم. حالا اگر آن را اجرا کنیم، به آمار و پلن زیر دست خواهیم یافت:

بله تعداد Page‌های خوانده شده این بار به 68 افزایش یافته است که نسبت به حالت بدون LastName که 2 Page خوانده شده بود، زیاد است. همانطور که در پلن زیر مشخص است، به دلیل ایندکسی که برروی FirstName ایجاد کرده‌ایم، نمی‌تواند تمام داده‌های مورد نیاز کوئری را مهیا کند. عملیات Key Lookup و nested loop هم این بار اضافه شده‌اند. Sql Server همچنان استفاده از ایندکس موجود را در کنار Key Lookup از ایندکس خوشه دار، ارزان‌تر از اسکن ایندکس خوشه دار، تشخیص داده است.

مشکل زمانی گریبان گیر ما خواهد شد که به ازای هر مطابقتی در ایندکس غیر خوشه دار، یک بار به ایندکس خوشه دار برای بررسی شرط بعدی و واکشی دیتا، رجوع خواهد شد. باید دقت کرد که Key Lookup همیشه به عنوان مشکل مطرح نمی‌شود. ولی باعث افزایش غیرضروری هزینه‌های CPU و I/O برای کوئری خواهد شد.

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

CREATE INDEX IX_Contacts_LastName ON dbo.Contacts(LastName);

اگر این بار کوئری قبل را اجرا کنیم، به آمار و پلن زیر خواهیم رسید:

بله تعداد Page‌های خوانده شده به 5 کاهش یافته و این بار به جای استفاده از Key Lookup، از دو index seek استفاده کرده است که هزینه‌ای کمتر را نسبت به حالت قبل خواهد داشت. به دلیل اینکه این دو ایندکس تمام دیتای لازم را می‌توانند مهیا کنند، دیگر نیازی به رجوع به ایندکس خوشه دار نخواهد بود. تصویر زیر در درک پلن بالا و این الگو می‌تواند مفید باشد:

Multiple Columns

در دو الگوی قبل، بیشتر به ایجاد ایندکس‌، بر روی یک ستون متمرکز شده بودیم. اگر تعدادی از ستون‌ها در بخش شروط مربوط به کوئری مطرح شوند، بهتر است آنها را در قالب یک ایندکس نگهداری کنیم. برای نشان دادن تأثیر این مورد،  یک ایندکس غیر خوشه دار را بر روی دو ستون ایجاد می‌کنیم: 

CREATE INDEX IX_Contacts_FirstNameLastName
    ON dbo.Contacts(FirstName, LastName);

SET STATISTICS IO ON;

SELECT ContactID,
       FirstName,
       LastName
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine'
       AND LastName = 'Cox';

SET STATISTICS IO OFF;

با اجرای کوئری بالا به آمار و پلن زیر خواهیم رسید:

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

الگوی Multiple Columns هم به مانند الگوی Search Columns باید هنگام ایندکس گذاری دیتابیس در نظر گرفته شود و از اهمیت بالایی برخوردار است. باید توجه داشت اگر فیلدهایی که در قسمت شرطی کوئری مطرح می‌شوند، متغییر باشد، استفاده از الگوی Index Intersection مفید خواهد. ولی برای مواقعی که نیاز است یکسری فیلد به صورت یکجا در بخش شرطی کوئری مطرح شوند، الگوی Multiple Columns کارآیی بهتری خواهد داشت. از این دو الگوی مطرح شده که در تناقض باهم قرار دارند، می‌توان به نحوی استفاده برد تا هزینه‌ی کلی را کاهش داد.

Covering Index

الگوی بعدی، ایندکس پوشش دهنده نام گرفته است. همانند نامی که دارد، هدف آن نگهداری یکسری ستون در ستون‌های ایندکس تولیدی که اتفاقا این ستون‌ها در قسمت شرطی کوئری قرار ندارند، ولی قرار است به عنوان خروجی Select برگردانده شوند، می‌باشد.
این الگو به عنوان یک روش استاندارد ایندکس گذاری در Sql Server مطرح بوده است. البته در ادامه و با بروز شدن روش‌هایی که می‌توان ایندکس‌ها را ایجاد کرد، این الگو نسبت به قبل کمتر مفید است! از آن جهت که یک روش شناخته شده می‌باشد، در این قسمت این مورد را هم مطرح کردیم. به مثال زیر توجه کنید:

SET STATISTICS IO ON;

SELECT ContactID,
       FirstName,
       LastName,
       IsActive
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine'
       AND LastName = 'Cox';

SET STATISTICS IO OFF;

در کوئری بالا این بار قصد داریم خصوصیت IsActive را که در ایندکس IX_Contacts_FirstNameLastName نگهداری نمی‌شود و همچنین در قسمت شرطی هم مطرح نشده و نیازی به آن نبوده، هم واکشی کنیم. با توجه به نتایج بدست آمده که در آمار و پلن زیر مشخص است، باز هم تعداد Page‌های خوانده شده به 5 افزایش یافته و بار دیگر، Key Lookup و Nested Loop را در کنار یک Index Seek، برروی ایندکسی که با الگوی Multiple Columns ایجاد کرده‌ایم، خواهیم داشت.


الگوی index covering پیشنهاد می‌کند ستونی را هم که در قسمت شرطی مطرح نمی‌شود، به عنوان ستونی اصلی در ایندکس، نگهداری کنیم؛ به شکل زیر:

CREATE INDEX IX_Contacts_FirstNameLastNameIsActive ON dbo.Contacts(FirstName, LastName,IsActive)

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

باز هم هزینه‌ی Key Lookup حذف شده و این بار از ایندکس جدید ما استفاده شده و تعداد Page‌های خوانده شده هم به 2 کاهش یافته است.
این الگو در بیشتر سناریو‌ها کاملا مفید بوده و پتانسیل افزایش کارآیی را در بیشتر سناریو‌ها دارد. اما در سال‌های اخیر از زمانیکه امکانات جدیدی در Sql Server 2005 به بعد ایجاد شد، از استفاده‌ی آن کاسته شده است. با وجود این امکانات جدید که در الگوی بعد به آن خواهیم پرداخت، می‌توان ستون‌های اضافی را در ایندکس‌ها، Include کنیم و نیازی نیست که جزء ستون‌های اصلی ایندکس باشند. 

Included Columns

الگوی Included Columns درواقعا پسر عموی الگوی Covering Index می‌باشد. در این الگو از عبارت INCLUDE در ایجاد یا تغییر ایندکس استفاده می‌شود و از این طریق امکان این را مهیا می‌کند تا یکسری ستون که جز ستون‌های اصلی ایندکس نیستند هم در ایندکس غیر خوشه دار ما افزوده شوند و حتی در قسمت شرطی هم مطرح شوند. این عمل خیلی شبیه به نگهداری دیتا‌های غیر کلیدی در یک ایندکس خوشه دار می‌باشد و این همان تفاوت اصلی بین دو الگو مطرح شده است.

اگر کوئری زیر را اجرا کنیم:

SET STATISTICS IO ON;

SELECT ContactID,
       FirstName,
       LastName,
       EmailAddress
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine';

SET STATISTICS IO OFF;

68 Page خوانده شده خواهیم داشت که حاصل یک Index Seek بر روی ایندکس IX_Contacts_FirstName می‌باشد و برای واکشی بقیه ستون‌ها هم یک Key Lookup بر روی ایندکس خوشه دار در پلن مشخص خواهد بود.

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

CREATE INDEX IX_Contacts_FirstNameINC ON dbo.Contacts(FirstName)
INCLUDE (LastName, IsActive, EmailAddress);

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

این بار از ایندکس جدید ایجاد شده استفاده شده و تعداد Page‌های خوانده شده، به 3 کاهش یافته است. با توجه به انعطاف پذیری این الگو می‌توان از اندک افزایشی که در تعداد Page‌های خوانده شده نسبت به الگوی ایندکس پوشش دهنده وجود دارد، چشم پوشی کرد.
در مثال‌های قبل چندین ایندکس بر روی جدول Contacts ایجاد کرده‌ایم که 4 مورد از آنها به صورت اختصاصی بر روی فیلد FirstName بوده است. باید توجه کرد این ایندکس‌ها نیاز به فضا و نگهداری در مواقع ویرایش رکورد‌های جدول خواهند داشت. لذا این هزینه‌ها اثر منفی برروی تمام عملیاتی خواهند داشت که روی جدول انجام می‌شود.
الگوی INC می‌تواند این مشکل را برطرف کند. برای مثال با استفاده از آن می‌توان ایندکس‌های تولید شده‌ی در مراحل قبل را بر روی FirstName، توسط یک ایندکس نیز پوشش داد. لذا ایندکس‌های قبلی را حذف کرده و با یکسری کوئری، مشخص خواهیم کرد که گفته‌ی ما صحت دارد:

IF EXISTS(SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.Contacts')
AND name = 'IX_Contacts_FirstNameLastName')
DROP INDEX IX_Contacts_FirstNameLastName ON dbo.Contacts
GO
IF EXISTS(SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.Contacts')
AND name = 'IX_Contacts_FirstNameLastNameIsActive')
DROP INDEX IX_Contacts_FirstNameLastNameIsActive ON dbo.Contacts
GO
IF EXISTS(SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('dbo.Contacts')
AND name = 'IX_Contacts_FirstName')
DROP INDEX IX_Contacts_FirstName ON dbo.Contacts
GO

با کدهای بالا ایندکس‌هایی را که بر روی FirstName ایجاد شده بودند، حذف کرده و این بار تمام کوئری‌های مطرح شده‌ی در مراحل قبل را یکبار دیگر اجرا می‌کنیم:

SET STATISTICS IO ON;

SELECT ContactID,
       FirstName
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine';

SELECT ContactID,
       FirstName,
       LastName
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine'
       AND LastName = 'Cox';

SELECT ContactID,
       FirstName,
       LastName,
       IsActive
FROM   dbo.Contacts
WHERE  FirstName = 'Catherine'
       AND LastName = 'Cox';

SET STATISTICS IO OFF;

دو نکته‌ای که باید به آنها توجه کرد:

  1. کوئری‌ها بالا در مقایسه با الگوهای قبلی به چه شکلی اجرا خواهند شد؟
  2. توجه کردن به تعداد Page‌های خوانده شده
در جواب مورد اول، Sql Server از عملیات Index Seek برای فیلترینگ برروی FirstName استفاده کرده و اگر ستون دیگری هم در بخش شرطی کوئری آورده شده، باز هم از این نوع عملیات استفاده شده است. به عنوان مثلا در دو کوئری بعد، LastName هم در بخش شرطی مطرح شده است‌. دلیل این کار که باز هم از Index Seek استفاده می‌شود این است که بعد از اعمال فیلترینگ بر روی FirstName، حالا یکسری رکورد در اختیار داریم که اتفاقا به LastName آنها هم دسترسی هست و فقط رکورد‌ها براساس آن مرتب نشده اند و نیازی نیست به ایندکس خوشه دار دسترسی داشته باشیم. لذا می‌توان همینجا بر روی این فیلد هم فیلترینگ را اعمال کرد. به پلن زیر توجه کنید:

در جواب مورد دوم، با اینکه حدود 50% افزایش در تعداد Page‌های خوانده شده نسبت به حالتی که به صورت جدا از هم برای هر کوئری خاص یک ایندکس در نظر گرفته بودیم، داشته‌ایم ولی این تغییر کارآیی نمی‌تواند ساخت 4 ایندکس را به جای 1 ایندکس که تمام آنها را پوشش می‌دهد، توجیه کند! در حالیکه ما به کارآیی مورد نظر خود دست یافته‌ایم.

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

Filtered Indexes

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

بله همانطور که از نام این الگو نیز مشخص است، هدف آن کاهش تعداد رکوردهایی است که در ایندکس نگهداری می‌شوند. به دو کوئری زیر توجه کنید:
SET STATISTICS IO ON;

SELECT   ContactID,
         FirstName,
         LastName,
         CertificationDate
FROM     dbo.Contacts
WHERE    CertificationDate IS NOT NULL
ORDER BY CertificationDate;

SELECT   ContactID,
         FirstName,
         LastName,
         CertificationDate
FROM     dbo.Contacts
WHERE    CertificationDate BETWEEN '20050101' AND '20050201'
ORDER BY CertificationDate;

SET STATISTICS IO OFF;
در کوئری اول به دنبال رکورد هایی هستیم که CertificationDate آنها نال می‌باشد و در دومی هم به دنبال آنهایی هستیم که در یک بازه‌ی زمانی قرار دارند. از آمار و پلن زیر مشخص است که چون هیچ ایندکس غیر خوشه داری بر روی CertificationDate ایجاد نشده‌است، از Index Scan برروی ایندکس خوشه دار استفاده شده است که حاصل آن خوانده شدن 2866 عدد Page می‌باشد!

زمانیکه مقدار آن نال باشد، استفاده نخواهد شد. آیا عقل سلیم قبول می‌کند که این مقادیر نال را در ایندکس نگهداری و رکوردهایی با مقادیر نال داشته باشیم؟ برای پیاده سازی این الگو باید از عبارت Where به هنگام ساخت ایندکس‌های غیر خوشه‌ای استفاده کنیم.
 توجه کنید که امکان استفاده از مقادیر متغیر در بخش Where، وجود ندارد.
نکته‌ی بعدی این است که نمی‌توان مقایسه‌های پیچیده را در این مورد استفاده کرد. برای مثال استفاده از LIKE و BETWEEN امکان پذیر نیست.

این بار با استفاده از الگوی Filtered Indexes یک ایندکس غیر خوشه‌ای را بر روی ستون CertificationDate ایجاد می‌کنیم:

CREATE INDEX IX_Contacts_CertificationDate ON dbo.Contacts(CertificationDate)
INCLUDE (FirstName, LastName)
WHERE CertificationDate IS NOT NULL;

حال دوباره دو کوئری قبلی را اجرا می‌کنیم. آمار و پلن زیر نشان می‌دهند که این بار فقط 2 عدد Page خوانده شده است و عملیات به Index Seek بر روی ایندکس جدید تغییر کرده است.


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

  • کم شدن تعداد رکورد‌های ایندکس‌ها موجب کاهش تعداد Page‌های مورد نیاز برای ذخیره سازی آنها و در نتیجه کاهش حجم مورد نیاز برای ذخیره سازی خواهد شد.
  • با توجه به مورد اول، اگر تعداد Page‌های برای نگهداری ایندکس کم باشند، لذا فرصت Fragmentation برای ایندکس کم خواهد بود و در نتیجه، هزینه و تلاش کمی برای نگهداری آن لازم است.
  • زمانیکه تعداد مقادیر نگهداری شده‌ی در ایندکس محدود هستند، تعداد Page هایی که برای پیمایش نیاز است، کم خواهند بود و اینجاست که حتی Index Scan هم بروری آن خیلی بهینه‌تر از Index Scan بر روی ایندکس خوشه دار می‌باشد.
شرایطی که می‌توان و باید از Filtered Indexes استفاده کرد:
  • اگر لازم است بر روی یک ستون که به‌صورت نال‌پذیر است، ایندکس ایجاد کنید(دلایل آن پیش‌تر گفته شد).
  • اگر لازم است برروی Sparse Column، یک ایندکس یکتا ایجاد کنید.
  • مورد بعدی همان بحث کاهش تعداد رکوردهایی می‌باشد که در ایندکس ذخیره می‌شوند.
Foreign Keys
آخرین الگویی که به آن می‌پردازیم مربوط می‌شود به کلید خارجی. این مورد تنها الگویی است که به طور مستقیم به اشیاء موجود در طراحی دیتابیس مربوط می‌باشد. کلید‌های خارجی گاهی مواقع می‌توانند باعث بروز مشکلی کارآیی شوند، بدون آنکه کسی متوجه این دخالت در کارآیی باشد.
از آنجائیکه کلید خارجی یک قید را بر روی مقادیر مجاز برای یک ستون مهیا می‌کند، لذا یک بررسی برای زمانیکه مقادیر نیاز به اعتبارسنجی دارند، وجود خواهد داشت. این اعتبارسنجی با توجه به شکل زیر دو نوع می‌باشد که به شرح زیر است:

  1. اعتبارسنجی بر روی جدول ParentTable  
  2. اعتبارسنجی بر روی جدول ChildTable 

در مورد نوع اول، هر وقت که رکوردهای جدول ChildTable تغییر کند، در این صورت مقدار ParentID موجود جدول ChildTable با یک جستجو در جدول ParentTable اعتبارسنجی خواهد شد. از آنجایی که این کلید خارجی در جدول ParentTable یک کلید اصلی بوده، یک ایندکس خوشه دار بر روی آن ایجاد شده است و تأثیری در کاهش کارآیی نخواهد داشت.
در مورد نوع دوم، هروقت تغییراتی بر روی  ParentID موجود در جدول ParentTable داشته باشیم، نیاز است اعتبار سنجی بر روی جدول ChildTable انجام شود. برای مثال با حذف یک رکورد در جدول پدر، لازم است که جدول فرزند بررسی کند که آیا این ParentID در رکورد‌ها موجود استفاده شده است یا خیر؟ در این نوع از اعتبارسنجی، الگوی Foreign Key خود را نشان می‌دهد.

برای نشان دادن استفاده‌ی از این الگو، لازم است جداول مطرح شده‌ی در تصویر بالا را ایجاد کنیم:

USE AdventureWorks2012;


GO
CREATE TABLE dbo.Customer (
    CustomerID  INT        ,
    FillterData CHAR (1000),
    CONSTRAINT PK_Customer_CustomerID PRIMARY KEY CLUSTERED (CustomerID)
);

CREATE TABLE dbo.SalesOrderHeader (
    SalesOrderID INT        ,
    OrderDate    DATETIME   ,
    DueDate      DATETIME   ,
    CustomerID   INT        ,
    FillterData  CHAR (1000),
    CONSTRAINT PK_SalesOrderHeader_SalesOrderID PRIMARY KEY CLUSTERED (SalesOrderID),
    CONSTRAINT GK_SalesOrderHeader_CustomerID_FROM_Customer FOREIGN KEY (CustomerID) REFERENCES dbo.Customer (CustomerID)
);

کد T-SQL بالا دو جدول مشتری و سفارش را ایجاد کرده و یک ارتباط یک به چند مابین آنها را از سمت مشتری به سفارش ایجاد می‌کند. برای انجام آزمایش خود، یکسری دیتای موجود را هم از جداول دیتابیس AdventureWorks2012 در جداول بالا درج می‌کنیم:

INSERT INTO dbo.Customer (CustomerID)
SELECT CustomerID
FROM   Sales.Customer;

INSERT INTO dbo.SalesOrderHeader (SalesOrderID, OrderDate, DueDate, CustomerID)
SELECT SalesOrderID,
       OrderDate,
       DueDate,
       CustomerID
FROM   Sales.SalesOrderHeader;

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

SET STATISTICS IO ON;

DELETE dbo.Customer
WHERE  CustomerID = 701;

SET STATISTICS IO OFF;

آمار و پلن زیر نشان می‌دهد که برای حذف یک رکورد در جدول مشتری، چون از عملیات Index Seek برروی ایندکس خوشه دار موجود برروی ستون CustomerID استفاده شده است، تنها 3 Page خوانده شده‌است؛ ولی برای اعتبارسنجی برروی جدول سفارش، با خواندن 4513 page و انجام عملیات Index Scan برروی ایندکس خوشه دار باعث کاهش کارآیی شده است.

برای پیاده سازی الگوی کلیدخارجی یک ایندکس غیر خوشه‌ای را بر روی CustomerID در جدول سفارشات ایجاد می‌کنیم:

CREATE INDEX IS_SalesOrderHeader_CustomerID ON dbo.SalesOrderHeader(CustomerID)

اگر دوباره کوئری بالا را با یک CustomerID دیگر انجام دهیم، به نتایج بهتری دست خواهیم یافت. تعداد Page‌های خوانده شده‌ی برای اعتبارسنجی جدول سفارشات، به عدد 2 کاهش یافته است! و از یک عملیات Index Seek بر روی ایندکس ایجاد شده، استفاده شده است.

اگر از EF استفاده می‌کنید، در حال حاضر به غیر از الگوهای Filtered Indexes و Include Indexes، پیاده سازی بقیه الگوهای ذکر شده به صورت توکار پشتیبانی می‌شود. برای دو الگوی مذکور هم می‌توان از نوشتن T-SQL خام استفاده کرد. برای مثال:

public partial class AddIndexes : DbMigration
    {
        private const string IndexName = "IX_LogSamples";

        public override void Up()
        {
            Sql(String.Format(@"CREATE NONCLUSTERED INDEX [{0}]
                               ON [dbo].[Logs] ([SampleId],[Date])
                               INCLUDE ([Value])", IndexName));

        }

        public override void Down()
        {
            DropIndex("dbo.Logs", IndexName);
        }
    }

یا حتی خیلی تمیزتر و  با ایده گرفتن از این مطلب می‌توان به یک کد Refactoring friendly نیز دست یافت.

پ.ن: این مطلب خلاصه‌ای از فصل 8 کتاب  Expert Performance Indexing for SQL Server 2012  می‌باشد. 

مطالب
مسیریابی در Angular - قسمت هشتم - مسیرهای ثانویه
به چندین مسیر که در یک زمان و در یک سطح، نمایش داده می‌شوند، مسیرهای ثانویه (secondary routes) گفته می‌شوند و برای ساخت رابط‌های کاربری پیچیده مفید هستند. از آن‌ها می‌توان برای نمایش چندین پنل در یک صفحه استفاده کرد که هر کدام دارای محتوایی متفاوت، به همراه مسیریابی مستقل و خاص خودشان هستند؛ مانند ساخت یک صفحه‌ی مدیریتی. هرچند می‌توان این صفحه‌ی مدیریتی را با درج مستقیم کامپوننت‌های آن‌ها در یک صفحه نیز نمایش داد، اما اگر هر کدام نیاز به مسیریابی خاصی نیز جهت نمایش جزئیات آن‌ها داشته باشند، دیگر روش درج مستقیم کامپوننت‌ها توسط selector آ‌ن‌ها در صفحه پاسخگو نخواهد بود.


 مروری بر نحوه‌ی کارکرد مسیریابی اصلی برنامه

 به router-outlet ایی که در فایل قالب src\app\app.component.html قرار گرفته‌است، primary outlet می‌گویند. زمانیکه کاربر، برنامه را در مرورگر مشاهده می‌کند، با هربار کلیک بر روی یکی از لینک‌های منوی بالای سایت، قالب آن‌را در این primary outlet مشاهده می‌کند. اگر بخواهیم پنل دیگری را در همین صفحه و در همین سطح از نمایش، درج کنیم، نیاز به تعریف outlet دیگری است که به همراه مسیرهای ثانویه‌ای نیز خواهد بود.


تعریف یک router-outlet نامدار

با توجه به اینکه هر پنل به همراه مسیریابی ثانویه، نیاز به router-outlet خودش را خواهد داشت، مسیریاب برای اینکه بداند محتوای آن‌ها را در کجای صفحه درج کند، به نام‌های آن‌ها مراجعه می‌کند. به این ترتیب می‌توان چندین router-outlet را در یک سطح از نمایش تعریف کرد؛ اما هرکدام باید دارای نامی منحصربفرد باشند.
در مثال این سری می‌خواهیم پنلی را در سمت راست صفحه‌ی اصلی درج کنیم. برای تعریف آن در همان سطحی که router-outlet اصلی قرار دارد، نیاز است فایل src\app\app.component.html را ویرایش کنیم:
<div class="container">
  <div class="row">
    <div class="col-md-10">
      <router-outlet></router-outlet>
    </div>
    <div class="col-md-2">
      <router-outlet name="popup"></router-outlet>
    </div>
  </div>
</div>
در اینجا با استفاده از امکانات بوت استرپ، دو ستون را در قالب اصلی برنامه تعریف کرده‌ایم. ستون اول حاوی router-outlet اصلی برنامه است و ستون دوم جهت درج پنل پیام‌های برنامه تعریف شده‌است. این router-outlet دوم، با نام popup مشخص گردیده‌است.


افزودن ماژول جدید پیام‌های سیستم

در ادامه ماژول جدید پیام‌های سیستم را به همراه تنظیمات ابتدایی مسیریابی آن اضافه خواهیم کرد که در آن ماژول، مدیریت نمایش پیام‌های مختلفی در router-outlet ثانویه popup صورت خواهد گرفت:
 >ng g m message --routing
به این ترتیب دو فایل src\app\message\message-routing.module.ts و src\app\message\message.module.ts به برنامه اضافه می‌شوند.

در ادامه نیاز است MessageModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم (پیش از AppRoutingModule که حاوی مسیریابی catch all است):

import { MessageModule } from './message/message.module';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),

    ProductModule,
    UserModule,
    MessageModule,

    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

سپس کامپوننت جدید Message را به ماژول Message برنامه اضافه می‌کنیم:
 >ng g c message/message
که اینکار سبب به روز رسانی فایل message.module.ts جهت تکمیل قسمت declarations آن با MessageComponent نیز می‌شود.

پس از آن یک سرویس ابتدایی پیام‌های کاربران را نیز اضافه خواهیم کرد:
 >ng g s message/message -m message/message.module
که سبب افزوده شدن سرویس message.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول message.module.ts نیز می‌شود:
 installing service
  create src\app\message\message.service.spec.ts
  create src\app\message\message.service.ts
  update src\app\message\message.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل message.service.ts آن‌را به نحو ذیل تکمیل می‌کنیم:
import { Injectable } from '@angular/core';

@Injectable()
export class MessageService {
  private messages: string[] = [];
  isDisplayed = false;

  addMessage(message: string): void {
    let currentDate = new Date();
    this.messages.unshift(message + ' at ' + currentDate.toLocaleString());
  }
}
هدف از این سرویس، به اشتراک گذاری اطلاعات بین کامپوننت‌های مختلف برنامه است. هر قسمت از برنامه (هر کامپوننتی) می‌تواند این سرویس را در سازنده‌ی خود تزریق کرده و پیامی را به مجموعه‌ی پیام‌های موجود اضافه کند.

اکنون جهت تکمیل کامپوننت پیام‌ها، ابتدا فایل قالب message.component.html را به نحو ذیل تکمیل می‌کنیم:
<div class="row">
  <h4 class="col-md-10">Message Log</h4>
  <span class="col-md-2">
      <a class="btn btn-default"  (click)="close()">x</a>
   </span>
</div>
<div *ngFor="let message of messageService.messages; let i=index">
  <div *ngIf="i<10" class="message-row">
    {{ message }}
  </div>
</div>
به این ترتیب تنها 10 پیام از مجموعه پیام‌های سرویس پیام‌ها، توسط قالب این کامپوننت نمایش داده خواهد شد. یک دکمه‌ی بستن نیز در اینجا اضافه شده‌است.
کدهای کامپوننت این قالب به صورت ذیل است:
import { MessageService } from './../message.service';
import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';

@Component({
  //selector: 'app-message',
  templateUrl: './message.component.html',
  styleUrls: ['./message.component.css']
})
export class MessageComponent implements OnInit {

  constructor(private messageService: MessageService,
    private router: Router) { }

  ngOnInit() {
  }

  close(): void {
    // Close the popup.
    this.router.navigate([{ outlets: { popup: null } }]);
    this.messageService.isDisplayed = false;
  }
}
این کامپوننت سرویس پیام‌ها را در اختیار قالب خود قرار داده و همچنین یک دکمه‌ی بستن را نیز به همراه دارد که خاصیت isDisplayed  آن‌را false می‌کند.


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

ابتدا به فایل src\app\product\product-edit\product-edit.component.ts مراجعه کرده و سرویس جدید پیام‌ها را به سازنده‌ی آن تزریق می‌کنیم:
import { MessageService } from './../../message/message.service';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.css']
})
export class ProductEditComponent implements OnInit {

  constructor(private productService: ProductService,
    private messageService: MessageService,
    private route: ActivatedRoute,
    private router: Router) { }
سپس ابتدای متد onSaveComplete آن‌را جهت درج پیام‌های این کامپوننت تغییر می‌دهیم.
  onSaveComplete(message?: string): void {
    if (message) {
      this.messageService.addMessage(message);
    }


تنظیم مسیرهای ثانویه

نحوه‌ی تعریف مسیریابی‌های مرتبط با router-outletهای غیراصلی برنامه، همانند سایر مسیریابی‌های برنامه‌است؛ با این تفاوت که در اینجا خاصیت outlet نیز به تنظیمات مسیر اضافه خواهد شد. به این ترتیب مشخص خواهیم کرد که محتوای این مسیر باید دقیقا در کدام router-outlet نامدار، درج شود.
برای این منظور فایل src\app\message\message-routing.module.ts را گشوده و تنظیمات مسیریابی آن‌را که به صورت RouterModule.forChild تعریف می‌شوند (چون ماژول اصلی برنامه نیستند)، تکمیل خواهیم کرد:
const routes: Routes = [
  { path: 'messages', component: MessageComponent, outlet: 'popup' }
];
همانطور که مشاهده می‌کنید، تنها تفاوت آن‌ها با سایر تعاریف مسیریابی‌های برنامه، ذکر نام Outlet ایی است که باید قالب MessageComponent را نمایش دهد.


فعالسازی یک مسیر ثانویه

در اینجا نیز همانند سایر مسیریابی‌ها، از دایرکتیو routerLink برای فعالسازی مسیرهای ثانویه استفاده می‌کنیم؛ اما syntax آن کمی متفاوت است:
<a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Messages</a>

<a [routerLink]="['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }]">Messages</a>
در اینجا می‌توان سبب فعال شدن چندین outlet به صورت همزمان شد. به همین جهت از نام جمع outlets استفاده شده‌است. سپس در ادامه key/valueهایی که بیانگر نام outlet و سپس path آن‌ها هستند، ذکر می‌شوند.
در دومین لینک تعریف شده، ابتدا یک مسیر اصلی فعال شده و سپس یک مسیر ثانویه نمایش داده می‌شود.

یک نکته: هرچند به primary outlet نامی انتساب داده نمی‌شود، اما نام آن دقیقا primary است و می‌توان قسمت outlets را به صورت ذیل نیز تعریف کرد:
{ outlets: { primary: ['/products', product.id,'edit'], popup: ['summary', product.id] }}


در ادامه فایل src\app\app.component.html را ویرایش کرده و لینک Show Messages را به آن اضافه می‌کنیم:
    <ul class="nav navbar-nav navbar-right">
      <li *ngIf="authService.isLoggedIn()">
        <a>Welcome {{ authService.currentUser.userName }}</a>
      </li>
      <li>
         <a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Show Messages</a>
      </li>
که سبب نمایش لینک Show Messages در منوی بالای سایت می‌شود (تصویر فوق). در این حال اگر بر روی آن کلیک کنیم این پنل جدید به سمت راست صفحه اضافه می‌شود. برای آزمایش آن، محصولی را ویرایش کنید، تا پیام مرتبط با آن در این پنل نمایش داده شود.
آدرس آن نیز چنین شکلی را پیدا می‌کند:
 http://localhost:4200/products(popup:messages)
در اینجا مسیرثانویه داخل یک پرانتز نمایش داده شده‌است. در این حالت اگر به صفحات مختلف برنامه مراجعه کنیم، هنوز این قسمت داخل پرانتز حفظ می‌شود و نمایان خواهد بود.

اکنون می‌خواهیم قابلیت مخفی سازی این پنل را نیز پیاده سازی کنیم. به همین جهت از خاصیت isDisplayed سرویس پیام‌ها که توسط دکمه‌ی بستن MessageComponent مدیریت می‌شود، استفاده خواهیم کرد. بنابراین لینک جدیدی را که در فایل src\app\app.component.html اضافه کردیم، به نحو ذیل تغییر خواهیم داد:
      <li *ngIf="!messageService.isDisplayed">
          <a (click)="displayMessages()">Show Messages</a>
      </li>
      <li *ngIf="messageService.isDisplayed">
         <a (click)="hideMessages()">Hide Messages</a>
      </li>
ngIfها بر اساس مقدار isDisplayed، سبب درج و یا حذف لینک‌های نمایش و مخفی کردن پیام‌ها می‌شوند و چون این قالب اکنون از سرویس پیام‌ها استفاده می‌کند، نیاز است این سرویس را به کامپوننت آن نیز تزریق کنیم:

import { MessageService } from './message/message.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  constructor(private authService: AuthService,
    private router: Router,
    private messageService: MessageService) {
  }

  displayMessages(): void {
    this.router.navigate([{ outlets: { popup: ['messages'] } }]);
    this.messageService.isDisplayed = true;
  }

  hideMessages(): void {
    this.router.navigate([{ outlets: { popup: null } }]);
    this.messageService.isDisplayed = false;
  }
}
در اینجا تزریق سرویس پیام‌ها را به سازنده‌ی کامپوننت App مشاهده می‌کنید. همچنین دو متد جدید نمایش و مخفی سازی پیام‌ها نیز تعریف شده‌اند که این متدها در قالب این کامپوننت، به لینک‌های مرتبطی متصل هستند.
برای فعالسازی یک مسیرثانویه توسط متدهای برنامه، نیاز است از سرویس مسیریاب و متد navigate آن استفاده کرد که نمونه‌هایی از آن‌را در اینجا ملاحظه می‌کنید. پارامترهای ذکر شده‌ی در اینجا نیز همانند دایرکتیو routerLink هستند.

یک نکته: اگر به متد hideMessages دقت کنید، مقدار value کلید popup به نال تنظیم شده‌است. این مورد سبب خواهد شد تا outlet آن خالی شود. به این ترتیب متد hideMessages علاوه بر مخفی کردن لینک نمایش پیام‌ها، پنل آن‌را نیز از صفحه حذف می‌کند. شبیه به همین نکته در متد close کامپوننت پیام‌ها که دکمه‌ی بستن آن‌را به همراه دارد، پیاده سازی شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
روشی برای محدود کردن API ها که هر درخواست با یک Key جدید و منحصر به فرد قابل فراخوانی باشد ( Time-based One-time Password )
TOTPیک الگوریتمی است که از ساعت برای تولید رمزهای یکبارمصرف استفاده میکند. به این صورت که در هر لحظه یک کد منحصر به فرد تولید خواهد شد. اگر با برنامه Google Authenticator کار کرده باشید این مفهوم برایتان اشناست. 
در این مطلب میخواهیم سناریویی را پیاده سازی کنیم که برای فراخوانی API‌ها باید یک رمز منحصر به فرد همراه توکن ارسال کنند. برای انجام این کار هر کاربر و یا کلاینتی که بخواهد از API استفاده کند در ابتدا باید لاگین کند و بعد از لاگین یک ClientSecret به طول 16 کاراکتر به همراه توکن به او ارسال میکنیم. از ClientSecret برای رمزنگاری کد ارسال شده ( TOTP ) استفاده میکنیم. مانند Public/Private key یک Key ثابت در سمت سرور و یک Key ثابت در برنامه موبایل وجود دارد و هنگام رمزنگاری TOTP علاوه بر Key موجود در موبایل و سرور از ClientSecret خود کاربر هم استفاده میکنیم تا TOTP بوجود آمده کاملا منحصر به فرد باشد. ( مقدار TOTP را با استفاده از دو کلید Key و ClientSecret رمزنگاری میکنیم ).
در این مثال تاریخ فعلی را ( UTC )  قبل از فراخوانی API در موبایل میگیریم و Ticks آن را در یک مدل ذخیره میکنیم. سپس مدل که شامل تایم فعلی میباشد را سریالایز میکنیم و به string تبدیل میکنیم سپس رشته بدست آمده را با استفاده از الگوریتم AES رمزنگاری میکنیم با استفاده از Key ثابت و ClientSecret. سپس این مقدار بدست آمده را در هدر Request-Key قرار میدهیم. توجه داشته باشید باید ClientSecret هم در یک هدر دیگر به سمت سرور ارسال شود زیرا با استفاده از این ClientSecret عملیات رمزگشایی مهیا میشود. به عنوان مثال ساعت فعلی به صورت Ticks به این صورت میباشد "637345971787256752 ". این عدد از نوع long است و با گذر زمان مقدار آن بیشتر میشود. در نهایت مدل نهایی سریالایز شده به این صورت است :
{"DateTimeUtcTicks":637345812872371593}
و مقدار رمزنگاری شده برابر است با :
g/ibfD2M3uE1RhEGxt8/jKcmpW2zhU1kKjVRC7CyrHiCHkdaAmLOwziBATFnHyJ3
مدل ارسالی شامل یک پراپرتی به نام DateTimeUtcTicks است که از نوع long میباشد و این مدل را قبل از فراخوانی API ایجاد میکند و مقدار رمزنگاری شده بدست آمده را در هدر Request-Key قرار میدهد به همراه ClientSecret در هدر مربوط به ClientSecret. این عمل در سمت موبایل باید انجام شود و در سمت سرور باید با استفاده از ClientSecret ارسال شده و Key ثابت در سرور این هدر را رمزگشایی کنند. چون این کار زیاد وقت گیر نیست و نهایتا یک دقیقه اختلاف زمان بین درخواست ارسال شده و زمان دریافت درخواست در سرور وجود دارد, در سمت سرور مقدار بدست آمده ( مقدار ارسال شده DateTimeUtcTicks که رمزگشایی شده است ) را اینگونه حساب میکنیم که مقدار ارسال شده از یک دقیقه قبل زمان فعلی باید بیشتر یا مساوی باشد و از تاریخ فعلی باید کوچکتر مساوی باشد. به این صورت 
var dateTimeNow = DateTime.UtcNow;
var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
var expireTimeTo = dateTimeNow.Ticks;

string clientSecret = httpContext.Request.Headers["ClientSecret"].ToString();
string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);

if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
و در نهایت اگر مقدار ارسال شده در بین این بازه باشد به معنی معتبر بودن درخواست میباشد در غیر این صورت باید خطای مربوطه را به کاربر نمایش دهیم. در ادامه برای پیاده  پیاده سازی این سناریو از یک Middleware استفاده کرده‌ایم که ابتدا بررسی میکند آیا درخواست ارسال شده حاوی هدر Request-Key و ClientSecret میباشد یا خیر؟ اگر هدر خالی باشد یا مقدار هدر نال باشد، خطای 403 را به کاربر نمایش میدهیم. برای جلوگیری از استفاده‌ی مجدد از هدر رمزنگاری شده، هنگامیکه اولین درخواست به سمت سرور ارسال میشود، رشته‌ی رمزنگاری شده را در کش ذخیره میکنیم و اگر مجددا همان رشته را ارسال کند، اجازه‌ی دسترسی به API را به او نخواهیم داد. کش را به مدت 2 دقیقه نگه میداریم؛ چون برای هر درخواست نهایتا یک دقیقه اختلاف زمانی را در نظر گرفته‌ایم. 
 در ادامه اگر رشته‌ی رمزنگاری شده در کش موجود باشد، مجددا پیغام "Forbidden: You don't have permission to call this api" را به کاربر نمایش میدهیم؛ زیرا به این معناست که رشته‌ی رمزنگاری شده قبلا ارسال شده است. سپس رشته رمزنگاری شده را رمزگشایی میکنیم و به مدل ApiLimiterDto دیسریالایز میکنیم و بررسی میکنیم که مقدار Ticks ارسال شده از طرف موبایل، از یک دقیقه قبل بیشتر بوده و از زمان حال کمتر باشد. اگر در بین این دو بازه باشد، یعنی درخواست معتبر هست و اجازه فراخوانی API را دارد؛ در غیر این صورت پیغام 403 را به کاربر نمایش میدهیم.
مدل ApiLimiterDto   :
public class ApiLimiterDto
{
    public long DateTimeUtcTicks { get; set; }
}
میان افزار :
public class ApiLimiterMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _cache;

    public ApiLimiterMiddleware(RequestDelegate next, IDistributedCache cache)
    {
        _next = next;
        _cache = cache;
    }
    private const string requestKey = "Request-Key";
    private const string clientSecretHeader = "ClientSecret";
    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (!httpContext.Request.Headers.ContainsKey(requestKey) || !httpContext.Request.Headers.ContainsKey(clientSecretHeader))
        {
            await WriteToReponseAsync();
            return;
        }

        var requestKeyHeader = httpContext.Request.Headers[requestKey].ToString();
        string clientSecret = httpContext.Request.Headers[clientSecretHeader].ToString();
        if (string.IsNullOrEmpty(requestKeyHeader) || string.IsNullOrEmpty(clientSecret))
        {
            await WriteToReponseAsync();
            return;
        }
        //اگر کلید در کش موجود بود یعنی کاربر از کلید تکراری استفاده کرده است
        if (_cache.GetString(requestKeyHeader) != null)
        {
            await WriteToReponseAsync();
            return;
        }
        var dateTimeNow = DateTime.UtcNow;
        var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
        var expireTimeTo = dateTimeNow.Ticks;

        string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
        var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);

        if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
        {
            //ذخیره کلید درخواست در کش برای جلوگیری از استفاده مجدد از کلید
            await _cache.SetAsync(requestKeyHeader, Encoding.UTF8.GetBytes("KeyExist"), new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(2)
            });
            await _next(httpContext);
        }
        else
        {
            await WriteToReponseAsync();
            return;
        }

        async Task WriteToReponseAsync()
        {
            httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
            await httpContext.Response.WriteAsync("Forbidden: You don't have permission to call this api");
        }
    }
}
برای رمزنگاری و رمزگشایی، یک کلاس را به نام AesProvider ایجاد کرده‌ایم که عملیات رمزنگاری و رمزگشایی را فراهم میکند.
public static class AesProvider
{
    private static byte[] GetIV()
    {
        //این کد ثابتی است که باید در سمت سرور و موبایل موجود باشد
        return encoding.GetBytes("ThisIsASecretKey");
    }
    public static string Encrypt(string plainText, string key)
    {
        try
        {
            var aes = GetRijndael(key);
            ICryptoTransform AESEncrypt = aes.CreateEncryptor(aes.Key, aes.IV);
            byte[] buffer = encoding.GetBytes(plainText);
            string encryptedText = Convert.ToBase64String(AESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length));
            return encryptedText;
        }
        catch (Exception)
        {
            throw new Exception("an error occurred when encrypting");
        }
    }
    private static RijndaelManaged GetRijndael(string key)
    {
        return new RijndaelManaged
        {
            KeySize = 128,
            BlockSize = 128,
            Padding = PaddingMode.PKCS7,
            Mode = CipherMode.CBC,
            Key = encoding.GetBytes(key),
            IV = GetIV()
        };
    }
    private static readonly Encoding encoding = Encoding.UTF8;
    public static string Decrypt(string plainText, string key)
    {
        try
        {
            var aes = GetRijndael(key);
            ICryptoTransform AESDecrypt = aes.CreateDecryptor(aes.Key, aes.IV);
            byte[] buffer = Convert.FromBase64String(plainText);

            return encoding.GetString(AESDecrypt.TransformFinalBlock(buffer, 0, buffer.Length));
        }
        catch (Exception)
        {
            throw new Exception("an error occurred when decrypting");
        }
    }
}
متد Decrypt و Encrypt یک ورودی به نام key دریافت میکنند که از هدر ClientSecret دریافت میشود. در سمت سرور عملا عمل Decrypt انجام میشود و Encrypt برای این مثال در سمت سرور کاربردی ندارد. 
برای رمزنگاری با استفاده از روش AES، چون از 128 بیت استفاده کرده‌ایم، باید طول متغییر key برابر 16 کاراکتر باشد و IV هم باید کمتر یا برابر 16 کاراکتر باشد.
در نهایت برای استفاده از این میان افزار میتوانیم از MiddlewareFilter استفاده کنیم که برای برخی از api‌های مورد نظر از آن استفاده کنیم.
کلاس ApiLimiterPipeline :
public class ApiLimiterPipeline
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<ApiLimiterMiddleware>();
    }
}
نحوه استفاده از میان افزار برای یک اکشن خاص :
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [MiddlewareFilter(typeof(ApiLimiterPipeline))]
    public async Task<IActionResult> Get()
    {
        return Ok("Hi");
    }
}

مطالب
سخنان بزرگان!

اشکال زدایی (debug) یک کد چندین مرتبه از نوشتن آن سخت‌تر است. بنابراین اگر کد اولیه خود را بسیار هوشمندانه بنویسید، جهت اشکال زدایی آن به اندازه‌ی کافی باهوش نخواهید بود! (Brian Kernighan)

تنها دو نوع زبان برنامه نویسی وجود دارد: آنهایی که برنامه نویس‌ها از آن شکایت دارند و آن‌هایی که اصلا مورد استفاده قرار نمی‌گیرند! (Bjarne Stroustrup)

هر کسی می‌تواند کدی بنویسد که یک کامپیوتر آن‌را درک کند. یک برنامه نویس خوب کدی را می‌نویسد که برای سایر همکارانش قابل درک باشد. (Martin Fowler)

اندازه‌گیری درصد پیشرفت یک پروژه برنامه نویسی با شمارش تعداد سطرهای کدهای آن همانند اندازه گیری درصد پیشرفت ساخت یک هواپیما از طریق وزن کردن آن است! (Bill Gates)

برنامه نویسی سطح پایین (Low-level) روح برنامه نویس‌ها را جلا می‌بخشد! (John Carmack, ID software)

بزرگی واقعی با اندازه گیری مقدار آزادی که به دیگران عطا می‌کنید، سنجیده می‌شود و نه به اینکه چگونه دیگران را وادار می‌کنید تا آنچه را که مد نظر شما است اجرا کنند. (Larry Wall)

هیچگاه از gets و sprintf استفاده نکنید، در غیر اینصورت شیاطین به زودی به سراغ شما خواهند آمد! (FreeBSD Secure Programming Guidelines)

صحبت کردن ساده است. کدت رو نشون بده! (Linus Torvalds)

علوم رایانه هیچگاه شخصی را تبدیل به یک برنامه نویس خوب نمی‌کنند همانطور که مطالعه در مورد رنگ‌ها و قلم‌ها شما را تبدیل به یک نقاش خوب نمی‌کند. (Eric Raymond)

برنامه نویسی مانند س.ک.س است. یک اشتباه و سپس تحمل کردن و پشتیبانی آن تا آخر عمر! (Michael Sinz)

هیچ برنامه‌ای تا زمانیکه آخرین یوزر آن بمیرد به پایان نخواهد رسید! (از یک گروه پشتیبانی نرم افزار ناشناس!)

برنامه نویس‌های C هرگز نخواهند مرد. آن‌ها فقط تبدیل به void خواهند شد. (ناشناس)

پایان دنیای یونیکس 2 به توان 32 ثانیه پس از اول ژانویه 1970 است! (ناشناس)

زمانی‌ که کد می‌نویسید فرض کنید شخصی که قرار است در آینده از کدهای شما نگهداری کند یک دیوانه‌ی زنجیری است که آدرس خانه‌ی شما را می‌داند! (Rick Osborne)

سادگی یک برنامه یکی از شرایط قابل اطمینان بودن آن است. (Edsger Dijkstra)

یونیکس سیستم عامل ساده‌ای است، اما شما باید فرد باهوشی باشید تا بتوانید این سادگی را درک کنید! (Dennis Ritchie)

اگر به کامپایلر دروغ بگوئید او بعدا انتقام خواهد گرفت! (Henry Spencere)

پرل تنها زبان برنامه نویسی است که پیش و پس از رمزنگاری RSA به یک شکل به نظر می‌رسد! (Keith Bostic)

تنها دو صنعت هستند که به مصرف کنندگان خود "کاربر" می‌گویند: صنعت کامپیوتر و تجارت مواد مخدر! (ناشناس)

نظرات مطالب
BulkInsert در EF CodeFirst
سلام . ممنون از مقاله خوبتون. آیا استفاده از BulkInsert برای موارد تکی هم خوبه؟ مثلا:
person prs = new Person() {name = "ali"};
 db.BulkInsert(prs);
یا اینکه فقط برای موارد لیستی مورد استفاده قرار میگیره و کارآمد هست؟
نظرات مطالب
یکسان سازی ی و ک دریافتی حین استفاده از NHibernate
@Ali
خیلی امیدوار شدم ... :)
بله. ممنون. این یک باگ در پیاده سازی بود. هنگام استفاده از این گوش فرا دهنده‌ها، تنها به روز رسانی خواص یک موجودیت کافی نیست؛ بلکه باید حالت آن‌را هم به روز کرد تا NHibernate واقعا متوجه شود که چیزی تغییر کرده.
نسخه‌ی جدید را از همان آدرس قبلی ذکر شده دریافت کنید.