مطالب
بررسی الگوهای ایندکس‌های 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  می‌باشد. 

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

WebStorage در برابر کوکی ها
یکی از روش‌های سنتی ذخیره اطلاعات در سیستم کاربر، کوکی‌ها در بستر Http هستند. تفاوت‌های زیادی بین این دو وجود دارد که تعدادی از آن‌ها را در زیر بررسی می‌کنیم:

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

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

  • محدودیت حجمی
محدودیت کوکی‌ها و webstorage از نظر حجم ذخیره سازی بین مروگرهای مختلف، متفاوت است. ولی در حالت کلی در بیشتر مرورگرها محدودیت حجم 4 کیلوبایت برای کوکی‌ها موجود است. (این ابزار می تواند نهایت حجمی را که که مرورگر شما از کوکی پشتیبانی می‌کند، نشان دهد.)
در مورد webstorage استاندارد W3C محدودیتی اعلام نکرده است و تصمیم گیری بر سر این موضوع را به سازندگان مرورگرها واگذار کرده است. ولی در حالت کلی حجم بیشتری از کوکی را ذخیره میکند و عموما تا 5 مگابایت توانایی ذخیره سازی وجود دارد. بدین ترتیب حجم آن 124,527% بیشتر از کوکی‌ها است. (این ابزار می‌تواند نهایت حجمی را که مرورگر شما از webstorage پشتیبانی می‌کند، ببینید).

انواع Webstorage 
دو نوع متد ذخیره سازی در webstorage داریم:
  • session storage
  • local storage

  • SessionStorage
داده‌هایی که بدین صورت ذخیره می‌شوند تنها تا زمانی دوام آورده و پایدار هستند که session مرورگر فعال است؛ یعنی تا زمانیکه کاربر در سایت فعلی حضور دارد.
استفاده از این روش برای ذخیره سازی‌های موقت عالی است. برای نمونه مقادیر ورودی فرمی که کاربر در حال کار با آن است، میتواند به طور موقت ذخیره شوند تا زمانی که کاربر بتواند تمامی مراحل را طی کرده، بدون اینکه ارجاعی به دیتابیس سمت سرور داشته باشد. همچنین ذخیره موقت داده‌ها می‌تواند به کاربر کمک کند تا در صورت reresh‌های ناگهانی یا بسته شدن‌های ناگهانی مرورگرها، نیازی به ورود مجدد داده‌ها نداشته باشد.
  • LocalStorage
در این روش داده‌ها با از دست رفتن session مرورگر جاری از بین نمی‌رود و برای بازدیدهای آتی کاربر از سایت، داده‌ها همچنان در دسترس هستند.
پشتیبانی مرورگرها
وب سایت caniuse گزارش می‌دهد که اکثر مرورگرها پشتیبانی خوبی از webstorage دارند.

با اینکه توصیه نامه W3C از پایان کار پیاده سازی این قابلیت خبر میدهد ولی در حال حاضر که این مقاله تدوین شده است هنوز نهایی اعلام نشده است. برای پشتیبانی مرورگرهای قدیمی از webstorage می‌توان از فایل جاوااسکریپتی Store.js کمک گرفت.


مفاهیم امنیتی و محافظت از داده ها

محدودیت‌های حمایتی و حفاظتی webstorage دقیقا همانند کوکی هاست. به این معنی که وب سایت‌های دیگر توانایی اتصال به webstorage سایت دیگری را ندارند. البته این مورد ممکن است برای وب سایت هایی که بر ساب دومین تکیه کرده‌اند ایجاد مشکل کند. برای حل این مسائل می‌توانید از کتابخانه‌های سورس بازی چون Cross Storage که توسط Zendesk ارائه شده است، استفاده کرد.

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

Data Integrity یا یکپارچگی داده‌ها نیز در نظر گرفته شده است. باید حفاظتی در برابر عدم موفقیت ذخیره سازی داده‌ها نیز وجود داشته باشد. این عدم موفقیت‌ها میتواند به دلایل زیر رخ دهد:

  • اگر کاربر قابلیت webstorage را غیرفعال کرده باشد.
  • اگر فضایی برای کاربر باقی نمانده باشد.
  • با محدودیت حجمی webstorage مواجه شده است.
  • با مواجه شدن با خطاها یک استثنا صادر می‌شود که می‌توانید آن را دریافت و کنترلی را روی برنامه تحت وب داشته باشید. یک نمونه استثنا  QuotaExceededError  


IndexedDB

یکی از فرایندهای ذخیره سازی داده‌ها که همان مزایای webstorage را ارائه میدهد indexed Database API است. این قابلیت از HTML 5 اضافه شده است و قسمتی از مشخصات webstorage شناخته نمی‌شود. برای همین مستنداتی در حوزه‌ی webstorage برای آن پیدا نخواهید کرد ولی قابلیت‌هایی فراتر از webstorage  دارد.

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

در قسمت آتی نحوه کدنویسی آن را فرا خواهیم گرفت.

مطالب
کار با Docker بر روی ویندوز - قسمت ششم - کار با بانک‌های اطلاعاتی درون Containerها
تا اینجا نحوه‌ی اجرای برنامه‌ها را داخل کانتینرها بررسی کردیم؛ اما هنوز در مورد داده‌های آن‌ها بحث نکرده‌ایم. اگر بانک‌های اطلاعاتی را به درون کانتینرها منتقل کنیم، چه بر سر داده‌های آن‌ها می‌آید؟


بررسی روش اجرای MS SQL Server Express درون یک Container

اگر مخازن Imageهای رسمی مایکروسافت را در داکرهاب بررسی کنیم، به مخازنی مانند mssql-server-windows-express ، mssql-server و یا mssql-server-linux نیز خواهیم رسید. در اینجا آخرین نگارش Image مربوط به SQL Server Express آن، حدود 7GB حجم دارد. برای دریافت آن ابتدا به Windows Containers سوئیچ کنید و سپس دستور زیر را صادر نمائید:
 docker pull microsoft/mssql-server-windows-express
پس از دریافت آن، اگر به مستندات رسمی آن در داکر هاب مراجعه کنیم، دستوری را به صورت زیر برای اجرای آن عنوان کرده‌است:
 docker run -d -p 1433:1433 -e sa_password=<SA_PASSWORD> -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
در این دستور:
- سوئیچ d سبب می‌شود تا پس از اجرای این دستور، بلافاصله به command prompt بازگشت داده شویم و SQL Server Express در background اجرا شود.
- سپس پورت 1433 میزبان به پورت 1433 کانتینر، نگاشت شده‌است که پورت استاندارد SQL Server است.
- سوئیچ e، امکان تنظیم متغیرهای محیطی را میسر می‌کند؛ برای مثال ورود کلمه‌ی عبور کاربر SA و یا پذیرش مجوز آن. برای نمونه، این کلمه‌ی عبور را مساوی password وارد کنید؛ هرچند کار نخواهد کرد، اما بررسی خطاهای به همراه آن مفید است.
- و در آخر نام image مرتبط ذکر شده‌است.

پس از اجرای این دستور، کانتینر SQL Server Express، در پس زمینه شروع به کار خواهد کرد و بلافاصله به خط فرمان بازگشت داده می‌شویم. در اینجا ممکن است آغاز SQL Server اندکی طول بکشد. برای اینکه دریابیم در این لحظه وضعیت پروسه‌ی آن به چه صورتی است، دستور docker logs id را صادر کنید. پس از آن خطایی مانند password validation failed را مشاهده خواهیم کرد. عنوان می‌کند که پیچیدگی کلمه‌ی عبور وارد شده کافی نیست.

یک نکته: زمانیکه دستور docker run را اجرا می‌کنیم، یک هش طولانی را نمایش می‌دهد و پس از آن به خط فرمان بازگشت داده می‌شویم. این هش طولانی، همان id کانتینر در حال اجرا است. برای مثال در دستور docker logs id می‌توان 3 حرف ابتدای این هش را بجای id وارد کرد. البته این id را توسط دستور docker ps نیز می‌توان بدست آورد.

بنابراین با توجه به اینکه دستور docker logs id، خطایی را گزارش کرده‌است، توسط دستور docker stop id، این کانتینر را متوقف کرده و آن‌را مجددا با کلمه‌ی عبوری مانند pass!w0rd1 اجرا می‌کنیم:
 docker run -d -p 1433:1433 -e sa_password=pass!w0rd1 -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
اینبار نیز مجددا دستور docker logs id را بر اساس id جدید این کانتینر اجرا می‌کنیم که پیام Started SQL Server را نمایش می‌دهد. بنابراین تا به اینجا موفق شدیم پروسه‌ی SQL Server Express را بدون مشکلی آغاز کنیم.


همانطور که در قسمت سوم نیز عنوان شد، اگر این کانتینر را بر روی ویندوز سرور، در حالت Windows Containers اجرا کنیم (و نه در حالت Hyper-V)، پروسه‌های اجرای شده‌ی داخل یک Container را می‌توان با Job Object Idهای یکسانی که دارند، در Task Manager ویندوز، در کنار سایر پروسه‌های سیستم، شناسایی کرد.


اتصال به SQL Server Express اجرا شده‌ی داخل یک Container توسط SQL Server Management Studio

پس از اجرای SQL Server Express دخل کانتینر، مطابق تنظیمات آن، چه در سیستم میزبان و چه در داخل کانتینر، به پورت 1433 گوش فرا داده می‌شود. به همین منظور نیاز است IP این کانتینر را نیز بدست آوریم. برای اینکار دستور ipconfig را در سیستم میزبان صادر کنید تا بر اساس مشخصات کارت شبکه‌ی مجازی آن، بتوان IP آن‌را بدست آورد (دستور docker inspect id نیز چنین اطلاعاتی را به همراه دارد). اکنون می‌توان از داخل سیستم راه دور دیگری که SQL Server Management Studio بر روی آن نصب است، توسط این IP و پورت، به SQL Server Express متصل شد.


البته در اینجا نیازی به ذکر پورت نیست؛ چون پورت 1433، شماره پورت پیش‌فرض است. بعد از اتصال، می‌توان کارهای متداولی مانند ایجاد یک بانک اطلاعاتی جدید را انجام داد.
برای آزمایش، یکبار دستور docker ps را صادر کنید تا id این کانتینر مشخص شود. سپس دستور docker stop id را صادر کنید تا پروسه SQL Server Express خاتمه یابد. اکنون اگر در SQL Server Management Studio قصد کار با آن‌را داشته باشیم، پیام عدم اتصال مشاهده می‌شود. اکنون برای اجرای مجدد کانتینر، دستور docker start id را صادر کنید.


بررسی روش اجرای MySQL داخل یک Container

برای اجرای MySQL نیاز است به Linux Containers سوئیچ کنیم. حجم tag ویژه‌ی latest آن نیز حدود 138MB است که نسبت به SQL Server Express هفت گیگابایتی، بسیار کمتر است!
در همان صفحه‌ی مستندات آن در داکرهاب، دستور اجرایی آن نیز ذکر شده‌است:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
در اینجا نیز توسط سوئیچ e که مخفف environment است، یکسری از متغیرهای محیطی MySQL، مانند کلمه‌ی عبور آن قابل تنظیم هستند. همچنین سوئیچ d نیز برای اجرای آن در پس زمینه، ذکر شده‌است. همین دستور را به همین شکل، صرفا با حذف tag آن، جهت اشاره‌ی به آخرین نگارش موجود این image، اجرا می‌کنیم:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql
با اجرای این دستور، در ابتدا MySQL از داکرهاب دریافت شده و سپس در پس زمینه اجرا خواهد شد. پیش از بازگشت به command prompt، یک هش طولانی نیز نمایش داده می‌شود که همان id کانتینر در حال اجرای آن است. برای اینکه بتوانیم ریز جزئیات رخ داده را بهتر مشاهده کنیم، می‌توان از دستور docker logs id استفاده کرد.

یک نکته: می‌توان یک command prompt جدید را باز کرد و سپس دستور docker logs -f id را در آن صادر کرد. به این صورت لاگ‌های لحظه‌ای یک کانتینر را نیز می‌توان مشاهده کرد (f در اینجا به معنای follow است).

اکنون می‌خواهیم MySQL Client موجود در همین Container در حال اجرا را، اجرا کنیم (اجرای پروسه‌ای درون یک کانتینر در حال اجرا). برای اینکار از دستور docker exec استفاده می‌شود:
docker ps
docker exec -it id mysql --user=root --password=my-secret-pw
ابتدا توسط دستور docker ps مقدار id این کانتینر را بدست می‌آوریم و سپس در دستور بعدی، از آن استفاده خواهیم کرد.
در اینجا توسط دستور docker exec ابتدا یک interactive shell را درخواست کرده‌ایم (اجرای foreground یک برنامه‌ی شل). سپس id این کانتینر باید ذکر شود. پس از آن نام فایل اجرایی MySQL Client قید شده و در پایان، نام کاربری و کلمه‌ی عبور اتصال به آن که در دستور docker run تنظیم شده‌اند، ذکر می‌شوند.
با اجرای این دستور، به خط فرمان MySQL Client داخل این کانتینر دسترسی پیدا می‌کنیم. در اینجا می‌توان دستورات مختلفی را برای کار با پروسه‌ی mysql اجرا کرد؛ مانند اجرای دستور show databases که لیست بانک‌های اطلاعاتی موجود را نمایش می‌دهد:
mysql> show databases;
use mysql;
show tables;
select * from user;
exit;


روش مدیریت داده‌های بانک‌های اطلاعاتی توسط Docker

در قسمت قبل دریافتیم که لایه‌ی رویی یک container، دارای قابلیت read/write است و برای مثال می‌توان فایل‌های یک وب سایت استاتیک را در آنجا کپی و سپس هاست کرد. اما این لایه، لایه‌ی مناسبی برای ذخیره سازی داده‌های یک بانک اطلاعاتی نیست. در اینجا برای مدیریت بهتر این نوع داده‌ها، از مفهومی به نام volume استفاده می‌شود.
برای درک روش مدیریت داده‌ها توسط داکر، دستور docker volume ls را اجرا کنید. مشاهده خواهید کرد که docker یک volume پیش‌فرض را نیز ایجاد کرده‌است. البته با volumes پیشتر در قسمت چهارم، در بخش «روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها» نیز آشنا شده‌ایم. این volume پیش‌فرض، کار ذخیره سازی اطلاعات را حتی اگر کانتینری در حال اجرا نباشد نیز انجام می‌دهد. وجود یک چنین قابلیتی جهت از دست نرفتن اطلاعات ارزشمند ذخیره شده‌ی در بانک‌های اطلاعاتی بسیار ضروری است.
البته لازم به ذکر است، این volume ای را که در اینجا مشاهده می‌کنید، توسط Dockerfile خود mysql به صورت خودکار ایجاد می‌شود. برای مثال در داکرهاب، در قسمت full description این image، در ابتدای توضیحات قسمتی است به نام supported tags and respective dockerfile links. در اینجا هر tag نامبرده شده، در حقیقت لینکی است به یک Dockerfile. اگر یکی از آن‌ها را باز کنید، چنین سطری را در آن مشاهده خواهید کرد:
  VOLUME /var/lib/mysql
این دستور سبب می‌شود چنین مسیری (مسیر پیش‌فرض ثبت اطلاعات mysql) به صورت یک volume جدید، خارج از فایل سیستم کانتینر، بر روی سیستم میزبان، ایجاد شود. سپس این مسیر و volume جدید، توسط داکر به صورت خودکار به این کانتینر mount خواهد شد و برای این موارد نیازی نیست کار خاصی توسط ما انجام شود.
اینکار نه فقط برای بالابردن کارآیی اعمال read/write انجام شده‌ی توسط container انجام می‌شود، بلکه حتی اگر این کانتینر را توسط دستور docker rm id حذف کنیم، دستور docker volume ls، هنوز همان volume ای را که در حین نصب mysql به صورت خودکار ایجاد شده بود، نمایش می‌دهد. علت اینجا است که طول عمر این volume، وابسته‌ی به طول عمر کانتینر آن نیست. به این ترتیب حذف تصادفی یک کانتینر، سبب از دست رفتن اطلاعات ارزشمند داخل بانک اطلاعاتی آن نمی‌شود.


روش تعیین صریح یک volume برای یک کانتینر بانک اطلاعاتی، توسط volumeهای نامدار

دستور docker run ای را که برای اجرای mysql صادر کردیم، یک volume خودکار را ایجاد کرده‌است و اگر آن‌را با دستور docker volume ls بررسی کنیم، دارای یک نام هش مانند است که به آن anonymous volume هم گفته می‌شود. در ادامه قصد داریم یک volume نامدار را ایجاد کنیم و سپس از آن جهت ذخیره سازی اطلاعات چندین وهله از کانتینر mysql استفاده نمائیم.
پیش از ادامه بحث، ابتدا توسط دستور docker rm id، کانتینر mysql ای را که پیشتر ایجاد کردیم حذف کنید؛ هرچند این دستور، volume متناظر با آن‌را حذف نمی‌کند.
سپس برای اینکه یک کانتینر جدید mysql را با ذکر صریح volume آن ایجاد و اجرا کنیم، می‌توان از دستور زیر استفاده کرد:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
در اینجا از سوئیچ v برای ایجاد یک volume نامدار استفاده شده‌است و در آن بجای ذکر قسمت مسیر پوشه‌ای در سمت میزبان، صرفا یک نام، مانند db، پیش از ذکر : قید شده‌است. پس از :، مسیری که این volume قرار است در آن کانتینر به آن نگاشت شود، ذکر شده‌است.
اکنون اگر دستور docker volume ls را صادر کنیم، در لیست خروجی آن، نام db قابل مشاهده‌است.

در ادامه پروسه‌ی MySQL Client داخل این کانتینر را اجرا کرده:
 docker exec -it some-mysql mysql --user=root --password=my-secret-pw
و تغییراتی را به صورت زیر اعمال می‌کنیم:
mysql> show databases;
create database pets;
show databases;
exit;
در اینجا بانک اطلاعاتی جدید pets ایجاد شده‌است.

اکنون در ابتدا این کانتینر را متوقف کرده و سپس آن‌را حذف می‌کنیم:
docker ps
docker stop id
docker rm id
هرچند اگر دستور حذف را با سوئیچ f- نیز اجرا کنیم (به معنای force)، کار stop را به صورت خودکار انجام می‌دهد.

در ادامه مجددا همان دستور قبلی را که توسط آن volume نامداری، ایجاد کردیم، اجرا می‌کنیم:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
اینبار اگر دستور docker volume ls را مجددا صادر کنیم، مشاهده خواهیم کرد این کانتینر جدید، بجای ایجاد یک volume جدید، از همان volume موجود db که آن‌را پیشتر ایجاد کردیم، استفاده می‌کند؛ هرچند کانتینری که آن‌را ایجاد کرده‌است، دیگر وجود خارجی ندارد. در این حالت اگر MySQL Client این کانتینر را اجرا نمائیم:
 docker exec -it some-mysql mysql --user=root --password=my-secret-pw
و سپس دستور نمایش بانک‌های اطلاعاتی آن‌را صادر کنیم:
 mysql> show databases;
در خروجی آن هنوز بانک اطلاعاتی pets که پیشتر ایجاد شده بود، قابل مشاهده‌است. بنابراین حذف و یا ایجاد کانتینرها، تاثیری را بر روی volumeهای ایجاد شده، نخواهند داشت.


روش حذف volumes اضافی

با توجه به اینکه volumeها، طول عمر متفاوتی را نسبت به کانتینرها دارند، ممکن است پس از مدتی فضای دیسک سخت شما را پر کنند. برای مثال به ازای هربار اجرای دستور docker run مربوط با MYSQL با نامی متفاوت، یک volume جدید نیز ایجاد می‌شود.
خروجی دستور docker inspect id به همراه قسمتی است به نام mounts که خاصیت name آن، دقیقا مساوی نام volume متناظر با کانتینر بررسی شده‌است. همچنین خاصیت source آن، محل دقیق ذخیره سازی این volume را بر روی فایل سیستم میزبان مشخص می‌کند.
برای حذف آن‌ها، ابتدا نیاز است کانتینرها را متوقف کرد. دستور زیر تمام کانتینرهای در حال اجرا را متوقف می‌کند. در اینجا دستور docker ps -q، لیست id تمام کانتینرهای در حال اجرا را باز می‌گرداند (در این دستورات، افزودن پارامتر q، سبب بازگشت صرفا idها می‌شود):
 docker stop $(docker ps -q)
اگر می‌خواهید تمام کانتینرهای موجود را حذف کنید:
 docker rm $(docker ps -aq)
و یا دستور زیر ابتدا تمام کانتینرهای موجود را متوقف کرده و سپس آن‌ها را حذف می‌کند:
 docker rm -f $(docker ps -aq)
دستور زیر تمام volumes موجود را حذف می‌کند:
 docker volume rm $(docker volume ls -q)
دستور زیر یک کانتینر با id مشخص شده را به همراه volume نامگذاری نشده‌ی مرتبط با آن، متوقف و سپس حذف می‌کند:
 docker rm -fv id
دستور زیر، لیست تمام volumes غیراستفاده شده‌ی توسط کانتینرهای موجود را نمایش می‌دهد (به یک چنین volumeهای در اینجا dangling گفته می‌شود؛ volume ای که کانتینر آن حذف شده‌است):
 docker volume ls -f dangling=true
 که می‌تواند لیست مناسبی برای حذف باشند:
 docker volume rm $(docker volume ls -qf dangling=true)
مطالب
بررسی ابزار SQL Server Profiler

مقدمه

Profiler یک ابزار گرافیکی برای ردیابی و نظارت بر کارآئی SQL Server است. امکان ردیابی اطلاعاتی در خصوص رویدادهای مختلف و ثبت این داده‌ها در یک فایل (با پسوند trc) یا جدول برای تحلیل‌های آتی نیز وجود دارد. برای اجرای این ابزار مراحل زیر را انجام دهید:

Start > Programs> Microsoft SQL Server > Performance Tools> SQL Server Profiler
و یا در محیط  Management Studio از منوی Tools گزینه SQL Server Profiler را انتخاب نمائید.


1- اصطلاحات

1-1- رویداد (Event):

یک رویداد، کاری است که توسط موتور بانک اطلاعاتی (Database Engine) انجام می‌شود. برای مثال هر یک از موارد زیر یک رویداد هستند.
-  متصل شدن کاربران (login connections) قطع شدن ارتباط یک login
-  اجرای دستورات T-SQL، شروع و پایان اجرای یک رویه، شروع و پایان یک دستور در طول اجرای یک رویه، اجرای رویه‌های دور Remote Procedure Call
-  باز شدن یک Cursor
-  بررسی و کنترل مجوزهای امنیتی

1-2- کلاس رویداد (Event Class):

برای بکارگیری رویدادها در Profiler، از یک Event Class استفاده می‌کنیم. یک Event Class رویدادی است که قابلیت ردیابی دارد. برای مثال بررسی ورود و اتصال کاربران با استفاده از کلاس Audit Login قابل پیاده سازی است. هر یک از موارد زیر یک Event Class هستند.
-  SQL:BatchCompleted
-  Audit Login
-  Audit Logout
-  Lock: Acquired
-  Lock: Released

1-3- گروه رویداد (Event Category):

یک گروه رویداد شامل رویدادهایی است که به صورت مفهومی دسته بندی شده اند. برای مثال، کلیه رویدادهای مربوط به قفل‌ها از جمله Lock: Acquired (بدست آوردن قفل) و Lock: Released (رها کردن قفل) در گروه رویداد Locks قرار  دارند.

1-4- ستون داده ای (Data Column):

یک ستون داده ای، خصوصیت و جزئیات یک رویداد را شامل می‌شود. برای مثال در یک Trace که رویدادهای Lock: Acquired را نظارت می‌کند، ستون Binary Data شامل شناسه (ID) یک صفحه و یا یک سطر قفل شده است و یا اینکه ستون Duration مدت زمان اجرای یک رویه را نمایش می‌دهد.

1-5- الگو (Template):

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

1-6- ردیاب (Trace):

یک Trace داده‌ها را براساس رویدادهای انتخاب شده، جمع آوری می‌کند. امکان اجرای بلافاصله یک Trace برای جمع آوری اطلاعات با توجه به رویدادهای انتخاب شده و ذخیره کردن آن برای اجرای آتی وجود دارد.

1-7- فیلتر (Filter):

هنگامی که یک Trace یا الگو ایجاد می‌شود، امکان تعریف شرایطی برای فیلتر کردن داده‌های جمع آوری شده نیز وجود دارد. این کار باعث کاهش حجم داده‌های گزارش شده می‌شود. برای مثال اطلاعات مربوط به یک کاربر خاص جمع آوری می‌شود و یا اینکه رشد یک بانک اطلاعاتی مشخص بررسی می‌شود.


2- انتخاب الگو (Profiler Trace Templates)

از آنجائیکه اصولاً انتخاب Eventهای مناسب، کار سخت و تخصصی می‌باشد برای راحتی کار تعدادی Template‌های آماده وجود دارد، برای مثال TSQL_Duration تاکیدش روی مدت انجام کار است و یا SP_Counts در مواردی که بخواهیم رویه‌های ذخیره شده را بهینه کنیم استفاده می‌شود در جدول زیر به شرح هر یک پرداخته شده است:
 الگو  هدف 
 Blank   ایجاد یک Trace کلی 
 SP_Counts   ثبت اجرای هر رویه ذخیره شده برای تشخیص اینکه هر رویه چند بار اجرا شده است 
 Standard   ثبت آمارهای کارائی برای هر رویه ذخیره شده و Query‌های عادی SQL که اجرا می‌شوند و عملیات ورود و خروج هر Login (پیش فرض) 
 TSQL   ثبت یک لیست از همه رویه‌های ذخیره شده و Query‌های عادی SQL که اجرا می‌شوند ولی آمارهای کارائی را شامل نمی‌شود 
 TSQL_Duration   ثبت مدت زمان اجرای هر رویه ذخیره شده و هر Query عادی SQL 
 TSQL_Grouped   ثبت تمام  login‌ها و logout‌ها در طول اجرای رویه‌های ذخیره شده و هر Query عادی SQL، شامل اطلاعاتی برای شناسائی برنامه و کاربری که درخواست را اجرا می‌کند 
 TSQL_Locks   ثبت اطلاعات انسداد (blocking) و بن بست (deadlock) از قبیل blocked processes، deadlock chains، deadlock graphs,... . این الگو همچنین درخواست‌های تمام رویه‌های ذخیره شده و تمامی دستورات هر رویه و  هر Query عادی SQL را دریافت می‌کند 
 TSQL_Replay   ثبت اجرای رویه‌های ذخیره شده و Query‌های SQL در یک SQL Instance و  مهیا کردن امکان اجرای دوباره عملیات در سیستمی دیگر 
 TSQL_SPs   ثبت کارائی برای Query‌های SQL، رویه‌های ذخیره شده و تمامی دستورات درون یک رویه ذخیره شده و نیز عملیات ورود و خروج هر Login 
 Tuning   ثبت اطلاعات کارائی برای Query‌های عادی SQL و رویه‌های ذخیره شده و یا تمامی دستورات درون یک رویه ذخیره شده 

3- انتخاب رویداد (SQL Trace Event Groups)

رویداد‌ها در 21 گروه رویداد دسته بندی می‌شوند که در جدول زیر لیست شده اند:
 گروه رویداد  هدف 
 Broker  13 رویداد برای واسطه سرویس (Service Broker) 
 CLR   1 رویداد برای بارگذاری اسمبلی‌های CLR (Common Language Runtime) 
 Cursors   7 رویداد برای ایجاد، دستیابی و در اختیار گرفتن Cursor 
 Database   6 رویداد برای رشد/کاهش  (grow/shrink) فایل های  Data/Log همچنین تغییرات حالت انعکاس (Mirroring) 
 Deprecation   2 رویداد برای آگاه کردن وضعیت نابسامان درون یک SQL Instance 
 Errors and
Warnings 
 16 رویداد برای خطاها، هشدارها و پیغام‌های اطلاعاتی که ثبت شده است 
 Full Text   3  رویداد برای پیگیری یک شاخص متنی کامل 
 Locks   9 رویداد برای بدست آوردن، رها کردن قفل و بن بست (Deadlock) 
 OLEDB   5 رویداد برای درخواست‌های توزیع شده و RPC (اجرای رویه‌های دور) 
 Objects   3 رویداد برای وقتی که یک شی ایجاد، تغییر یا حذف می‌شود 
 Performance   14 رویداد برای ثبت نقشه درخواست‌ها (Query Plan) برای استفاده نقشه راهنما (Plan Guide) به منظور بهینه سازی کارائی درخواست ها،  همچنین این گروه رویداد در خواست‌های متنی کامل (full text) را ثبت می‌کند 
 Progress Report   10 رویداد برای ایجاد Online Index 
 Query
Notifications 
 4 رویداد برای سرویس اطلاع رسان (Notification Service) 
 Scans   2 رویداد برای وقتی که یک جدول یا شاخص، پویش می‌شود 
 Security Audit   44 رویداد برای وقتی که مجوزی استفاده شود، جابجائی هویتی رخ دهد، تنظیمات امنیتی اشیائی تغییر کند،یک  SQL Instance  شروع و متوقف شود و یک  Database جایگزین شود یا از آن پشتیبان گرفته شود 
 Server  3 رویداد برای Mount Tape، تغییر کردن حافظه سرور و بستن یک فایل Trace 
 Sessions   3 رویداد برای وقتی که Connection‌ها موجود هستند و یک Trace فعال می‌شود، همچنین یک Trigger  و یک تابع دسته بندی(classification functions) مربوط به مدیریت منابع(resource governor) رخ دهد 
 Stored Procedures   12 رویداد برای اجرای یک رویه ذخیره شده و دستورات درون آن ، کامپایل مجدد و استفاده از حافظه نهانی (Cache) 
 Transactions   13 رویداد برای شروع، ذخیره ، تائید و لغو یک تراکنش 
 TSQL   9  رویداد برای اجرای Query‌های SQL و جستجوهای XQUERY (در داده‌های XML)  
 User Configurable   10 رویداد که شما می‌توانید پیکربندی کنید 
به طور معمول بیشتر از گروه رویدادهای Locks، Performance، Security Audit، Stored Procedures و TSQL استفاده می‌شود.


4- انتخاب ستون‌های داده ای ( Data Columns)

اگرچه می‌توان همه‌ی 64 ستون داده ای ممکن را برای ردیابی انتخاب کرد ولیکن داده‌های Trace شما زمانی مفید خواهند بود که اطلاعات ضروری را ثبت کرده باشید. برای مثال شماره ترتیب تراکنش‌ها را،  برای یک رویداد RPC:Completed می‌توانید برگردانید، اما همه رویه‌های ذخیره شده مقادیر را تغییر نمی‌دهند بنابراین شماره ترتیب تراکنش‌ها فضای بیهوده ای را مصرف می‌کند. بعلاوه همه ستون‌های داده ای برای تمامی رویداد‌های Trace معتبر نیستند. برای مثال Read ، Write ،CPU و Duration برای رویداد‌های RPC:Starting و SQL:BatchStarting معتبر نیستند.
ApplicationName، NTUserName، LoginName، ClientProcessID، SPID، HostName، LoginSID، NTDomainName و SessionLoginName ، مشخص می‌کنند چه کسی و از چه منشاء دستور را اجرا کرده است.
ستون SessionLoginName معمولاً نام Login ای که از آن برای متصل شدن به SQL Instance استفاده شده است را نشان می‌دهد. در حالیکه ستون LoginName نام کاربری را که دستور را اجرا می‌کند نشان می‌دهد (EXECUTE AS). ستون ApplicationName خالی است مگر اینکه در ConnectionString برنامه کاربردیمان این خصوصیت (Property) مقداردهی شده باشد. ستون StartTime و EndTime زمان سرحدی برای هر رویداد را ثبت می‌کند این ستون‌ها بویژه در هنگامی که به عملیات Correlate  نیاز دارید مفید هستند.


5- بررسی چند سناریو نمونه

•  یافتن درخواست هائی (Queries) که بدترین کارایی را دارا هستند.

برای ردیابی درخواست‌های ناکارا، از رویداد RPC:Completed از دسته Stored Procedure و رویداد SQL:BatchCompleted از دسته TSQL استفاده می‌شود.

•  نظارت بر کارایی رویه ها

برای ردیابی کارائی رویه ها، از رویدادهای SP:Starting، SP:Completed، SP:StmtCompleted و SP:StmtStaring از کلاس Stored Procedure و رویدادهای SQL:BatchStarting ، SQL:BatchCompleted از کلاس TSQL استفاده می‌شود.

•  نظارت بر اجرای دستورات T-SQL توسط هر کاربر

برای ردیابی دستوراتی که توسط یک کاربر خاص اجرا می‌شود، نیاز به ایجاد یک Trace برای نظارت بر رویدادهای کلاس‌های Sessions، ExistingConnection و TSQL داریم همچنین لازم است نام کاربر در قسمت فیلتر  و با استفاده از DBUserName مشخص شود.

•  اجرا دوباره ردیاب (Trace Replay)

این الگو  معمولاً برای debugging استفاده می‌شود برای این منظور  از الگوی Replay استفاده می‌شود. در ضمن امکان اجرای دوباره عملیات در سیستمی دیگر با استفاده از این الگو مهیا می‌شود.

•  ابزار Tuning Advisor (راهنمای تنظیم کارائی)

این ابزاری برای تحلیل کارائی یک یا چند بانک اطلاعاتی و تاثیر عملکرد آنها بر بار کاری (Workload) سرویس دهنده است. یک بار کاری مجموعه ای از دستورات T-SQL است که روی بانک اطلاعاتی اجرا می‌شود. بعد از تحلیل تاثیر بارکاری بر بانک اطلاعاتی، Tuning Advisor توصیه هائی برای اضافه کردن، حذف و یا تغییر طراحی فیزیکی ساختار بانک اطلاعاتی ارائه می‌دهد این تغییرات ساختاری شامل پیشنهاد برای تغییر ساختاری موارد Clustered Indexes، Nonclustered Indexes، Indexed View و Partitioning است.
برای ایجاد بارکاری می‌توان از یک ردیاب تهیه شده در SQL Profiler استفاده کرد برای این منظور از الگوی Tuning استفاده می‌شود و یا رویدادهای RPC:Completed، SQL:BatchCompleted و SP:StmtCompleted را ردیابی نمائید.

•  ترکیب ابزارهای نظارتی (Correlating Performance and Monitoring Data)

یک Trace برای ثبت اطلاعاتی که در یک SQL Instance رخ می‌دهد، استفاده می‌شود. System Monitor  برای ثبت شمارنده‌های کارائی(performance counters) استفاده می‌شود و همچنین از منابع سخت افزاری و اجزای دیگر که روی سرور اجرا می‌شوند، تصاویری فراهم می‌کند. توجه شود که در مورد  Correlating یک فایل ردیاب (trace file) و یک Counter Log (ابزار Performance )، ستون داده ای StartTime و EndTime باید انتخاب شود، برای این کار از منوی File گزینه Import Performance Data انتخاب می‌شود.

•  جستجوی علت رخ دادن یک بن بست

برای ردیابی علت رخ دادن یک بن بست، از رویدادهای RPC:Starting، SQLBatchStarting از دسته Stored Procedure و رویدادهای Deadlock graph، Lock:Deadlock و Lock:Deadlock Chain از دسته Locks استفاده می‌شود. ( در صورتی که نیاز به یک ارائه گرافیکی دارید از  Deadlock graph استفاده نمائید، خروجی مطابق تصویر زیر می‌شود).


5-1- ایجاد یک Trace

1-  Profiler را اجرا کنید از منوی File گزینه New Trace را انتخاب کنید و به SQL Instance مورد نظرتان متصل شوید.
2-  مطابق تصویر زیر برای Trace یک نام و الگو و تنظیمات ذخیره سازی فایل را مشخص کنید.


3-  بر روی قسمت Events Selection کلیک نمائید.
4-  مطابق تصویر زیر رویداد‌ها و کلاس رویداد‌ها را انتخاب کنید، ستون‌های TextData، NTUserName، LoginName، CPU،Reads،Writes، Duration، SPID، StartTime، EndTime، BinaryData، DataBaseName، ServerName و ObjectName را انتخاب کنید.

5-  روی Column Filters کلیک کنید و مطابق تصویر زیر برای DatabaseName فیلتری تنظیم کنید.


6-  روی Run کلیک کنید. تعدادی Query و رویه ذخیره شده مرتبط با پایگاه داده AdventureWorks اجرا کنید .


5-2- ایجاد یک Counter Log

برای ایجاد یک Counter Log  مراحل زیر  را انجام دهید:
1-  ابزار Performance را اجرا کنید (برای این کار عبارتPerfMon را در قسمت Run بنویسید).
2-  در قسمت Counter Logs یک log ایجاد کنید.
3-  روی Add Counters کلیک کرده و مطابق تصویر موارد زیر را انتخاب کنید.
Select counters from list 
Performance Object 
 Output Queue Length  Network Interface 
 % Processor Time  Processor 
 Processor Queue Length  System 
 Buffer Manager:Page life expectancy  SQLServer 
 
4-  روی Ok کلیک کنید تا Counter Log ذخیره شود سپس روی آن راست کلیک کرده و آنرا Start کنید.


5-3- ترکیب ابزارهای نظارتی (Correlating SQL Trace and System Monitor Data)

1-  Profiler را اجرا کنید از منوی File گزینه Open و سپس Trace File را انتخاب کنید فایل trc را که در گام اول ایجاد کردید، باز نمائید.
2-  از منوی File گزینه Import Performance Data را انتخاب کنید و فایل counter log  را که در مرحله قبل ایجاد کردید  انتخاب کنید.



نکته: اطلاعات فایل trc را می‌توان درون یک جدول وارد کرد، بدین ترتیب می‌توان آنالیز بیشتری داشت به عنوان مثال دستورات زیر این عمل را انجام می‌دهند.


 SELECT * INTO dbo.BaselineTrace
FROM fn_trace_gettable(' c:\performance baseline.trc ', default);
با اجرای دستور زیر جدولی با نام  BaselineTrace ایجاد و محتویات Trace مان (performance baseline.trc) در آن درج می‌گردد.
 
مطالب دوره‌ها
دریافت قالب WpfFramework.vsix و نحوه نصب و راه اندازی آن
فایل قالب پروژه دوره جاری را از آدرس ذیل می‌توانید دریافت کنید:

نصب ابتدایی آن بسیار ساده است و نکته خاصی ندارد.
پس از نصب، یکبار VS.NET را بسته و سپس باز کنید. این قالب جدید، ذیل قسمت پروژه‌های ویندوز مرتبط با ویژوال سی شارپ، ظاهر خواهد شد:


در این حال اگر یک پروژه جدید را آغاز کنید، این قالب تدارک دیده شده، لایه‌ها و قسمت‌های مختلف را به صورت خودکار اضافه خواهد کرد:



نکته مهم! برنامه کامپایل نمی‌شود!

به عمد، جهت کاهش حجم قالب دریافتی فوق، فایل‌های باینری آن پیوست نشده‌اند. وگرنه باید بالای 30 مگابایت را دریافت می‌کردید که واقعا نیازی نیست.
اما اگر به هر پروژه داخل Solution دقت کنید، فایل متنی packages.config آن نیز پیوست شده است. به کمک این فایل‌ها به سادگی می‌توان تمام وابستگی‌ها را از طریق NuGet بازیابی کرد.
برای این منظور ابتدا به اینترنت متصل شده (مهم) و سپس بر روی Solution کلیک راست کرده و گزینه فعال سازی Restore کلیه بسته‌های نیوگت را انتخاب کنید.


پس از اینکار، آخرین نگارش NuGet.exe از اینترنت دریافت و به پروژه اضافه می‌شود:


اکنون اگر Solution را Build کنید، اولین کاری که صورت خواهد گرفت، دریافت کلیه وابستگی‌ها از سایت NuGet است. اندکی تامل کنید تا اینکار تمام شود.
پس از پایان کار دریافت، پوشه packages در کنار فایل sln پروژه ایجاد شده، تشکیل می‌شود.
یکبار وجود آن‌را بررسی کنید.


اکنون اگر برنامه را Build کنید احتمالا پیغام می‌دهد که Fody را نمی‌تواند پیدا کند با اینکه دریافت شده و در پوشه packages موجود است. هر زمان، هر پیغام خطایی در مورد Fody مشاهده کردید، فقط یکبار VS.NET را بسته و باز کنید. مشکل حل می‌شود!

تنها کاری که پس از بازسازی پوشه packages بهتر است صورت گیرد (اختیاری است البته)، صدور دستور ذیل در خط فرمان پاور شل است:
 PM> Update-Package
با این دستور، دریافت کامل مجددی از اینترنت صورت نمی‌گیرد؛ مگر اینکه به روز رسانی جدیدی را یافت کند. در سایر حالات از کش داخل سیستم اطلاعات را دریافت می‌کند.
پس از اینکار، نیاز است یکبار VS.NET را بسته و مجددا باز کنید (مهم) تا تمام وابستگی‌ها به درستی بارگذاری شوند. خصوصا بسته Fody که کار AOP را انجام می‌دهد. در غیر اینصورت موفق به Build پروژه نخواهید شد.

بنابراین به صورت خلاصه:
الف) یکبار گزینه فعال سازی Restore کلیه بسته‌های نیوگت را انتخاب کنید.
ب) پروژه را Build کنید تا وابستگی‌ها را از سایت NuGet دریافت کند.
ج) دستور Update-Package را اجرا نمائید (اختیاری).
ج) VS.NET را پس از سه مرحله فوق، یکبار بسته و باز کنید.

در کل بسته‌های مورد استفاده به این شرح هستند:
PM> Install-Package MahApps.Metro -Pre 
PM> Install-Package MahApps.Metro.Resources 
PM> Install-Package EntityFramework
PM> Install-Package structuremap
PM> Install-Package PropertyChanged.Fody
PM> Install-Package MvvmLight 
PM> Install-Package Microsoft.SqlServer.Compact

یک نکته جانبی
فید NuGet در VS.NET به Https تنظیم شده است. اگر دسترسی به Https برای شما به کندی صورت می‌گیرد فقط کافی است مسیر فید آن‌را در منوی Tools، گزینه‌ی Options، ذیل قسمت Package manager یافته و به http://nuget.org/api/v2 تغییر دهید؛ یعنی به Http خالی، بجای Https؛ تا سرعت دریافت بسته‌های NuGet مورد نظر افزایش یابند.



اجرای پروژه و برنامه

پس از این مراحل و Build موفقیت آمیز پروژه، برنامه را اجرا کنید. در اولین بار اجرای برنامه به صورت خودکار بانک اطلاعاتی به همراه ساختار جداول تشکیل می‌شوند. اما ... با خطای زیر مواجه خواهید شد:
 The path is not valid. Check the directory for the database. [ Path = D:\...\bin\Debug\Db\db.sdf ]
علت این است که در فایل app.config پروژه ریشه برنامه :
<connectionStrings>
    <clear/>
    <add name="MyWpfFrameworkContext"
         connectionString="Data Source=|DataDirectory|\Db\db.sdf;Max Buffer Size=30720;File Mode=Read Write;"
         providerName="System.Data.SqlServerCE.4.0" />
  </connectionStrings>
مسیر تشکیل بانک اطلاعاتی به پوشه db کنار فایل اجرایی برنامه تنظیم شده است. این پوشه db را در پوشه bin\debug ایجاد کرده و سپس برنامه را اجرا کنید.


برای ورود به برنامه از نام کاربری Admin و کلمه عبور 123456 استفاده نمائید.
این کاربر پیش فرض در کلاس MyWpfFrameworkMigrations لایه داده‌های برنامه، توسط متد addRolesAndAdmin اضافه شده است.


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




به روز رسانی 1.1
جهت سازگاری با EF 6 ، StructureMap 3 و همچنین VS 2013، پروژه به روز شد: WpfFrmwork_1.1.zip

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

هر دو حالت فوق در PdfReport پشتیبانی می‌شوند و در ادامه نحوه انجام این موارد را بررسی خواهیم کرد.

الف) بارگذاری تصاویر از فایل سیستم

ابتدا مدل زیر را در نظر بگیرید:
namespace PdfReportSamples.Models
{
    public class ImageRecord
    {
        public int Id { set; get; }
        public string ImagePath { set; get; }
        public string Name { set; get; }
    }
}
توسط آن تعدادی رکورد را که ImagePath آن‌ها فایل‌هایی بر روی سیستم هستند، خواهیم ساخت:
using System;
using System.Collections.Generic;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.ImageFilePath
{
    public class ImageFilePathPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SnowyPineTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<ImageRecord>
                                             {
                                                 new ImageRecord
                                                     {
                                                         Id=1,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\01.png",
                                                         Name = "Rnd"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=2,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\02.png",
                                                         Name = "Bug"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=3,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\03.png",
                                                         Name = "Stuff"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=4,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\04.png",
                                                         Name = "Sun"
                                                     }
                                             };
                dataSource.StronglyTypedList(listOfRows);
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(3);
                    column.HeaderCell("شماره");
                    column.ColumnItemsTemplate(t => t.Barcode(new Barcode39()));
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.ImagePath);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("تصویر");
                    column.ColumnItemsTemplate(t => t.ImageFilePath(defaultImageFilePath: string.Empty, fitImages: false));
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("نام");
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptImageFilePathSample.pdf"));
        }
    }
}

توضیحات:
- در متد MainTableDataSource، یک سری رکورد دلخواه را بر اساس تعدادی تصویر مشخص ایجاد کرده‌ایم. این تصاویر در پوشه bin\images مثال‌های PdfReport قرار دارند. سپس آن‌ها را توسط متد dataSource.StronglyTypedList، در اختیار PdfReport قرار داده‌ایم.
- در مرحله بعد، توسط متد MainTableColumns، ستون‌های دلخواه گزارش را ایجاد کرده‌ایم.
دو نکته در اینجا مهم هستند:
الف) برای نمایش یک تصویر از فایل سیستم، فقط کافی است از ColumnItemsTemplate متناظر با آن استفاده کرد (حالت پیش فرض، نمایش متنی اطلاعات است). برای نمونه قالب پیش فرض ImageFilePath به نحو زیر قابل استفاده است:
 column.ColumnItemsTemplate(t => t.ImageFilePath(defaultImageFilePath: string.Empty, fitImages: false));
در اینجا در آرگومان اول آن می‌توان مسیر تصویری را مشخص کرد که در صورت موجود نبودن تصویر مشخص شده در منبع داده، بهتر است نمایش داده شود. اگر string.Empty وارد شد، از این مورد صرفنظر خواهد شد. آرگومان دوم آن مشخص می‌کند که آیا تصویر نمایش داده شده باید با ابعاد سلول متناظر با آن هماهنگ شده و نمایش داده شود یا خیر.
ب) در کتابخانه iTextSharp که پایه PdfReport است، امکان تهیه بارکد هم وجود دارد. برای مثال این بارکدها توسط قالب زیر، قابل استفاده خواهند شد:
 column.ColumnItemsTemplate(t => t.Barcode(new Barcode39()));
تصویری از حاصل این گزارش را در ذیل مشاهده می‌کنید:


ب) بارگذاری محتوای تصاویر از بانک اطلاعاتی

ابتدا کدهای کامل این مثال را در نظر بگیرید:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.DbImage
{
    public class DbImagePdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                            Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.AutumnTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.FitToContent);
             })
             .MainTableDataSource(dataSource =>
             {
                 dataSource.GenericDataReader(
                   providerName: "System.Data.SQLite",
                   connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                   sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate], [thumbnail]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                   parametersValues: new object[] { 10 }
               );
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع کل");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.HeaderCell("ردیف");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("url");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.HeaderCell("آدرس");
                 });

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

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("NumberOfPosts");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     column.HeaderCell("تعداد پست");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("AddDate");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                     column.HeaderCell("تاریخ ثبت");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : ((DateTime)obj).ToString("dd/MM/yyyy HH:mm"));
                     });
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("thumbnail");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(5);
                     column.HeaderCell("تصویر");
                     column.ColumnItemsTemplate(t => t.ByteArrayImage(defaultImageFilePath: string.Empty, fitImages: false));
                 });
             })
             .Export(export =>
             {
                 export.ToXml();
                 export.ToExcel();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptDbImageSample.pdf"));
        }
    }
}

توضیحات:
بانک اطلاعاتی SQLite موجود در پوشه bin\data سورس PdfReport، حاوی فیلدی است به نام thumbnail که در آن محتوای یک سری تصویر ذخیره شده‌اند.
در اینجا توسط کوئری زیر قصد داریم این اطلاعات را خوانده و نمایش دهیم:
SELECT [url], [name], [NumberOfPosts], [AddDate], [thumbnail]
FROM [tblBlogs]
WHERE [NumberOfPosts]>=@p1
همانطور که ملاحظه می‌کنید، در اینجا نیز تنها کافی است قالب ستون مرتبطی را انتخاب کنیم:
column.ColumnItemsTemplate(t => t.ByteArrayImage(defaultImageFilePath: string.Empty, fitImages: false));
قالب ByteArrayImage، اطلاعات باینری فیلد thumbnail را خوانده و تبدیل به تصاویر قرار داده شده در فایل گزارش نهایی می‌کند. پارامترهای آن، با پارامترهای قالب ImageFilePath که پیشتر توضیح داده شد، یکی هستند. 
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت یازدهم- استفاده از تامین کننده‌های هویت خارجی
همیشه نمی‌توان کاربران را وادار به استفاده‌ی از صفحه‌ی لاگین برنامه‌ی IDP کرد. ممکن است کاربران بخواهند توسط سطوح دسترسی خود در یک شبکه‌ی ویندوزی به سیستم وارد شوند و یا از Social identity providers مانند تلگرام، گوگل، فیس‌بوک، توئیتر و امثال آن‌ها برای ورود به سیستم استفاده کنند. برای مثال شاید کاربری بخواهد توسط اکانت گوگل خود به سیستم وارد شود. همچنین مباحث two-factor authentication را نیز باید مدنظر داشت؛ برای مثال ارسال یک کد موقت از طریق ایمیل و یا SMS و ترکیب آن با روش فعلی ورود به سیستم جهت بالا بردن میزان امنیت برنامه.
در این مطلب نحوه‌ی یکپارچه سازی Windows Authentication دومین‌های ویندوزی را با IdentityServer بررسی می‌کنیم.


کار با تامین کننده‌های هویت خارجی

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


نگاهی به شیوه‌ی پشتیبانی از تامین کننده‌های هویت خارجی توسط Quick Start UI

Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کننده‌های هویت خارجی نیز می‌باشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl)

[HttpGet]
public async Task<IActionResult> Callback()
زمانیکه کاربر بر روی یکی از تامین کننده‌های لاگین خارجی در صفحه‌ی لاگین کلیک می‌کند، اکشن Challenge، نام provider مدنظر را دریافت کرده و پس از آن returnUrl را به اکشن متد Callback به صورت query string ارسال می‌کند. اینجا است که کاربر به تامین کننده‌ی هویت خارجی مانند گوگل منتقل می‌شود. البته مدیریت حالت Windows Authentication و استفاده از اکانت ویندوزی در اینجا متفاوت است؛ از این جهت که از returnUrl پشتیبانی نمی‌کند. در اینجا اطلاعات کاربر از اکانت ویندوزی او به صورت خودکار استخراج شده و به لیست Claims او اضافه می‌شود. سپس یک کوکی رمزنگاری شده از این اطلاعات تولید می‌شود تا در ادامه از محتویات آن استفاده شود.
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شده‌ی متد Challenge استخراج می‌شود و بر اساس آن هویت کاربر در سطح IDP شکل می‌گیرد.


فعالسازی Windows Authentication برای ورود به IDP

در ادامه می‌خواهیم برنامه را جهت استفاده‌ی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامه‌های ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامه‌ی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمه‌ی جدید Windows را در قسمت External Login مشاهده خواهید کرد:


یک نکته: برچسب این دکمه را در حالت استفاده‌ی از مشتقات IIS، به صورت زیر می‌توان تغییر داد:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows Account";
                iis.AutomaticAuthentication = false;
            });

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

سازنده‌ی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آن‌را در قسمت قبل تغییر دادیم، از TestUserStore استفاده می‌کند:
        public ExternalController(
            IIdentityServerInteractionService interaction,
            IClientStore clientStore,
            IEventService events,
            TestUserStore users = null)
        {
            _users = users ?? new TestUserStore(TestUsers.Users);

            _interaction = interaction;
            _clientStore = clientStore;
            _events = events;
        }
بنابراین در ابتدا آن‌را با IUsersService تعویض خواهیم کرد:
        private readonly IUsersService _usersService;
        public ExternalController(
    // ...
            IUsersService usersService)
        {
    // ...
            _usersService = usersService;
        }
و سپس تمام ارجاعات قبلی به users_ را نیز توسط امکانات این سرویس اصلاح می‌کنیم:
الف) در متد FindUserFromExternalProvider
سطر قدیمی
 var user = _users.FindByExternalProvider(provider, providerUserId);
به این صورت تغییر می‌کند:
 var user = await _usersService.GetUserByProviderAsync(provider, providerUserId);
در این حالت امضای این متد نیز باید اصلاح شود تا async شده و همچنین User را بجای TestUser بازگشت دهد:
 private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)> FindUserFromExternalProvider(AuthenticateResult result)
ب) متد AutoProvisionUser قبلی
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims)
{
   var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList());
   return user;
}
نیز باید حذف شود؛ زیرا در ادامه آن‌را با صفحه‌ی ثبت نام کاربر، جایگزین می‌کنیم.
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحه‌ی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کرده‌ایم و از آن می‌شود در اینجا استفاده‌ی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کننده‌ی هویت خارجی را به نحوی اصلاح می‌کنیم که کاربر جدید، ابتدا به صفحه‌ی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامه‌ی مراحل را طی نماید:
در اکشن متد نمایش صفحه‌ی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحه‌ی قبلی را دریافت می‌کند:
[HttpGet]
public IActionResult RegisterUser(string returnUrl)
اکنون نیاز است اطلاعات Provider و ProviderUserId را نیز در اینجا دریافت کرد. به همین جهت ViewModel زیر را به برنامه اضافه می‌کنیم:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class RegistrationInputModel
    {
        public string ReturnUrl { get; set; }
        public string Provider { get; set; }
        public string ProviderUserId { get; set; }

        public bool IsProvisioningFromExternal => !string.IsNullOrWhiteSpace(Provider);
    }
}
سپس با داشتن اطلاعات FindUserFromExternalProvider که آن‌را در قسمت الف اصلاح کردیم، اگر خروجی آن null باشد، یعنی کاربری که از سمت تامین کننده‌ی هویت خارجی به برنامه‌ی ما وارد شده‌است، دارای اکانتی در سمت IDP نیست. به همین جهت او را به صفحه‌ی ثبت نام کاربر هدایت می‌کنیم. همچنین پس از پایان کار ثبت نام نیاز است مجددا به همینجا، یعنی متد Callback که فراخوان FindUserFromExternalProvider است، بازگشت:
namespace DNT.IDP.Controllers.Account
{
    [SecurityHeaders]
    [AllowAnonymous]
    public class ExternalController : Controller
    {
        public async Task<IActionResult> Callback()
        {
            var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
            var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user = AutoProvisionUser(provider, providerUserId, claims);
                
                var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl });
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" ,
                    new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId });
                return Redirect(continueWithUrl);
            }
در اینجا نحوه‌ی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحه‌ی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده می‌کنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره می‌کند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.

در ادامه نیاز است امضای متد نمایش صفحه‌ی ثبت نام را نیز بر این اساس اصلاح کنیم:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class UserRegistrationController : Controller
    {
        [HttpGet]
        public IActionResult RegisterUser(RegistrationInputModel registrationInputModel)
        {
            var vm = new RegisterUserViewModel
            {
                ReturnUrl = registrationInputModel.ReturnUrl,
                Provider = registrationInputModel.Provider,
                ProviderUserId = registrationInputModel.ProviderUserId
            };

            return View(vm);
        }
به این ترتیب اطلاعات provider نیز علاوه بر ReturnUrl در اختیار View آن قرار خواهد گرفت. البته RegisterUserViewModel هنوز شامل این خواص اضافی نیست. به همین جهت با ارث بری از RegistrationInputModel، این خواص در اختیار RegisterUserViewModel نیز قرار می‌گیرند:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class RegisterUserViewModel : RegistrationInputModel
    {

اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه می‌کنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آن‌ها نیاز داریم:
<inputtype="hidden"asp-for="ReturnUrl"/>
<inputtype="hidden"asp-for="Provider"/>
<inputtype="hidden"asp-for="ProviderUserId"/>
- با توجه به اینکه کاربر از طریق یک تامین کننده‌ی هویت خارجی وارد شده‌است، دیگر نیازی به ورود کلمه‌ی عبور ندارد. به همین جهت خاصیت آن‌را در ViewModel مربوطه به صورت Required تعریف نکرده‌ایم:
@if (!Model.IsProvisioningFromExternal)
{
    <div>
        <label asp-for="Password"></label>
        <input type="password" placeholder="Password"
               asp-for="Password" autocomplete="off">
    </div>
}
مابقی این فرم ثبت نام مانند قبل خواهد بود.

پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
namespace DNT.IDP.Controllers.UserRegistration
{
    public class UserRegistrationController : Controller
    {
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> RegisterUser(RegisterUserViewModel model)
        {
    // ...
            
            if (model.IsProvisioningFromExternal)
            {
                userToCreate.UserLogins.Add(new UserLogin
                {
                    LoginProvider = model.Provider,
                    ProviderKey = model.ProviderUserId
                });
            }

            // add it through the repository
            await _usersService.AddUserAsync(userToCreate);

// ...
        }
    }
که اینکار را با مقدار دهی UserLogins کاربر در حال ثبت، انجام داده‌ایم.
همچنین در ادامه‌ی این اکشن متد، کار لاگین خودکار کاربر نیز انجام می‌شود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کننده‌ی خارجی، غیرفعال می‌کنیم:
if (!model.IsProvisioningFromExternal)
{
    // log the user in
    // issue authentication cookie with subject ID and username
    var props = new AuthenticationProperties
    {
        IsPersistent = false,
        ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
    };
    await HttpContext.SignInAsync(userToCreate.SubjectId, userToCreate.Username, props);
}

بررسی ورود به سیستم توسط دکمه‌ی External Login -> Windows

پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینه‌ی External Login -> Windows را انتخاب می‌کنیم. بلافاصله به صفحه‌ی ثبت‌نام کاربر هدایت خواهیم شد:


همانطور که مشاهده می‌کنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کرده‌است. همچنین در اینجا خبری از ورود کلمه‌ی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامه‌ی MVC Client انجام می‌شود.
در ادامه از برنامه‌ی کلاینت logout کنید. اکنون در صفحه‌ی login مجددا بر روی دکمه‌ی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامه‌ی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت IDP انجام شده‌است.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.

یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامه‌ی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمی‌کنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکته‌ی تکمیلی: UseHttpSys و استفاده‌ی از HTTPS»  فعال کنید.
مطالب
چگونه نرم افزارهای تحت وب سریعتری داشته باشیم؟ قسمت اول
در این سلسله مقالات قصد دارم چندین مطلب راجع به افزایش سرعت نرم افزارهای تحت وب مطرح نمایم. این مطالب هرچند بسیار مختصر می‌باشند ولی در کارایی و سرعت برنامه‌های شما در آینده تاثیر خواهند داشت.
1.کش کردن همیشه آخرین حربه می‌باشد
این مهم است که بخش‌های مختلف سایت شما در سطوح مختلف کش شوند (ASP.NET, Kernel, Server, Proxy Server, Browser ,...) ولی این موضوع باید همیشه آخرین حربه و نکته ای باشد که آن را در مورد سایت خود اعمال می‌کنید.
یعنی همیشه مطمئن شوید ابتدا تمامی نکات مربوط به افزایش کارایی در برنامه خود را رعایت کرده اید، سپس اقدام به کش داده‌ها در سطوح مختلف نمایید. توجه کنید کش کردن داده‌ها و صفحات می‌تواند مشکلات را برای شما به عنوان یک برنامه نویس یا تست کننده برنامه پنهان کند و به شما اطمینان دهد که همه چیز خوب کار می‌کند در حالی که این چنین نیست!
البته ذکر این نکته هم بی فایده نیست که کش کردن همه چیز بعضی مواقع دشمن برنامه شما محسوب می‌شود! هیچ وقت یادم نمی‌رود، در پورتال داخلی یک شرکت که در وقت استراحت به کارکنان اجازه مطالعه روزنامه‌های روز را می‌داد (به صورت آفلاین)، این نکته در بالای صفحه آورده شده بود: «لطفا برای به روز رساندن صفحات روزنامه‌ها از کلید Ctrl+F5 استفاده نمایید». این موضوع یعنی بحث کشینگ در برنامه آن پرتال در سطح فاجعه می‌باشد! حالا فرض کنید این مشکل در فرم ورود و یا مرور اطلاعات یک برنامه به وجود آید...
2.حذف View Engine‌های غیر ضروری
به عنوان یک برنامه نویس ASP.NET MVC، یابد اطلاع داشته باشید که CLR به صورت خودکار View Engine‌های Razor و Web Forms را لود می‌کند. این موضوع به این دلیل است که اطلاعی از نحوه برنامه نویسی شما ندارد. اگر شما فقط از یکی از این دو View Engine استفاده می‌کنید،لطفا دیگری را غیر فعال کنید! فعال بودن هر دوی آنها یعنی اتلاف وقت گرانبهای CPU سرور شما برای رندر کردن تمامی صفحات شما توسط دو انجین! ایتدا view‌های شما با Web Forms Engine رندر شده سپس نتیجه به  Razor Engine منتقل شده و مجدد توسط این انجین رندر می‌شود. این موضوع در سایت‌های با تعداد کاربر بالا یعنی فاجعه!
برای حل این مشکل کافی است خطوط زیر را در فایل Global.asax و در رویداد بخش Application_Start وارد نمایید:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
این دو خط یعنی خداحافظ Web Forms Engine...
قبل از استفاده از این کد، اطمینان حاصل کنید کل برنامه شما توسط Razor تهیه شده است وگرنه بنده هیچ مسئولیتی در رابطه با فریادهای کارفرمای شما متقبل نمی‌شوم!
صد البته برای حذف Razor Engine و استفاده از Web Form Engine می‌توان از کد زیر در همان موقعیت فوق استفاده کرد:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new WebFormViewEngine());
البته همانطور که حتما دوستان مطلع هستند امکان گسترش Engine‌های فوق توسط ارث بری از کلاس BuildManagerViewEngine جهت ایجاد Engine‌های دیگر همیشه محیا است. در این صورت می‌توانید تنها انجین سفارشی مورد نظر خود را لود کرده و از لود دیگر انجین‌ها پرهیز کنید. 
3. استفاده از فایل‌های PDB مایکروسافت برای دیباگ و یا پروفایل کردن DLL‌های دیگران  
دیباگ یا پروفایل کردن برنامه ها، DLL ها، اسمبلی‌ها و منابعی از برنامه که شما آن را خود ننوشته اید (سورس آنها در دسترس شما نمی‌باشد) همیشه یکی از سخت‌ترین مراحل کار می‌باشد. جهت کمک به دیباگر‌ها یا پروفایلرها، نیاز است فایل‌های PDB مرتبط با DLL‌ها را در اختیار آنها قرار دهید تا به بهترین نتیجه دسترسی پیدا کنید. این فایل‌ها محتوی نام توابع، شماره خطوط برنامه و metadata‌های دیگر برنامه اصلی قبل از optimize شدن توسط کامپایلر یا JIT می‌باشد.  خوب حالا اگر نیاز شد این کار را در رابطه با DLL‌ها و کلاس‌های پایه Microsoft.NET انجام دهیم چه کار کنیم؟
خیلی ساده! خود Microsoft سروری جهت این موضوع تدارک دیده که فایل‌های PDB را جهت دیباگ کردن در اختیار تیم‌های برنامه نویسی قرار می‌دهد.کافی است از منوی Tools گزینه Options را انتخاب، سپس به بخش Debugging و به بخش Symbols بروید و گزینه Microsoft Symbol Servers as your source for Symbols را انتخاب کنید. برای اطمینان از اینکه هر مرتبه که برنامه را دیباگ می‌کنید مجبور به دانلود این فایل‌ها نشوید، فراموش نکنید پوشه ای را جهت کش این فایل‌ها ایجاد و آدرس آن را در بخش Cache symbols in this directory همین صفحه وارد نمایید.
این امکان در Visual Studio 2010, 2012 در دسترس می‌باشد.
نظرات مطالب
یکسان سازی ی و ک دریافتی حین استفاده از NHibernate
مایکروسافت مورد مشابهی در مورد الف و همزه برای عربی زبان‌ها دارد که مشابه اون رو می‌شه برای ی/ی ک/ک ما هم می‌شه درخواست داد.
در سیستم‌های مدیریت محتوای (غالبن متن‌باز ِ) با پشتیبانی فارسی مقل جوملا و سایر خلنواده‌هاش تبدیل خودکار حروف عربی به فارسی معمولن ساپورت می‌شه و از جمله مثلن یکی از پلاگین‌های پیش‌فرض وردپرس تبدیل خودکار ی/ک به ی/ک هستش.

+ یه موردی که بارها خواسته بودم ازتون سوال کنم و بحثش پیش نیومده بود اینه که شما خودتون از kbdfa عربی پورت شده از روی ویندوز98 هنوز استفاده می‌کنید که "ک" و "ی" عربیه.
چرا از برنامه‌های زیادی که برای این کار است استفاده نمی‌کنید یا خودتون dll مذکور رو جوری تعییر نمی‌دید که ک و ی هاتون فارسی باشه؟
یادمه چند سال پیش در جایی صحبت شد و توجیه‌تون این بود که مطالب با ی عربی ایندکس شده توی گوگل بیشتره و این استاندارد رایج وب فارسی هستش. برای اون موقع شاید این مطلب درستی بود ولی الان هر کلمه‌ای که خواستید رو سرچ کنید می‌بینید نتایج جستجوی فارسی‌ش برای "ی/ک" بیشتر از "ی/ک" هستش.
مطالب
پردازش داده‌های جغرافیایی به کمک SQL Server و Entity framework
پشتیبانی SQL Server از Spatial data

از SQL Server 2008 به بعد، نوع داده جدیدی به نام geography به نوع‌های قابل تعریف ستون‌ها اضافه شده‌است. در این نوع ستون‌ها می‌توان طول و عرض جغرافیایی یک نقطه را ذخیره کرد و سپس به کمک توابع توکاری از آن‌ها کوئری گرفت.


در اینجا نمونه‌ای از نحوه‌ی تعریف و همچنین مقدار دهی این نوع ستون‌ها را مشاهده می‌کنید:
 CREATE TABLE [Geo](
[id] [int] IDENTITY(1,1) NOT NULL,
[Location] [geography] NULL
)

 insert into Geo( Location , long, lat ) values
( geography::STGeomFromText ('POINT(-121.527200 45.712113)', 4326))
متد geography::STGeoFromText یک SQL CLR function است. این متد در مثال فوق، مختصات یک نقطه را دریافت کرده‌است. همچنین نیاز دارد بداند که این نقطه توسط چه نوع سیستم مختصاتی ارائه می‌شود. عدد 4326 در اینجا یک SRID یا Spatial Reference System Identifier استاندارد است. برای نمونه اطلاعات ارائه شده توسط Google و یا Bing توسط این استاندارد ارائه می‌شوند.
در اینجا متدهای توکار دیگری مانند geography::STDistance برای یافتن فاصله مستقیم بین نقاط نیز ارائه شد‌ه‌اند. خروجی آن بر حسب متر است.


پشتیبانی از Spatial Data در Entity framework

پشتیبانی از نوع مخصوص geography، در EF 5 توسط نوع داده‌ای DbGeography ارائه شد. این نوع داده‌ای immutable است. به این معنا که پس از نمونه سازی، دیگر مقدار آن قابل تغییر نیست.
در اینجا برای نمونه مدلی را مشاهده می‌کنید که از نوع داده‌ای DbGeography استفاده می‌کند:
using System.Data.Entity.Spatial;

namespace EFGeoTests.Models
{
    public class GeoLocation
    {
        public int Id { get; set; }
        public DbGeography Location { get; set; }
        public string Name { get; set; }
        public string Type { get; set; }

        public override string ToString()
        {
            return string.Format("Name:{0}, Location:{1}", Name, Location);
        }
    }
}
به همراه یک Context، تا کلاس GeoLocation در معرض دید EF قرار گیرد:
using System;
using System.Data.Entity;
using EFGeoTests.Models;

namespace EFGeoTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<GeoLocation> GeoLocations { get; set; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
برای مقدار دهی خاصیت Location از نوع DbGeography می‌توان از متد ذیل استفاده کرد که بسیار شبیه به متد geography::STGeoFromText عمل می‌کند:
   private static DbGeography createPoint(double longitude, double latitude,  int coordinateSystemId = 4326)
  {
       var text = string.Format(CultureInfo.InvariantCulture.NumberFormat,"POINT({0} {1})", longitude, latitude);
       return DbGeography.PointFromText(text, coordinateSystemId);
  }


تهیه منبع داده‌ی جغرافیایی

برای تدارک یک مثال واقعی جغرافیایی، نیاز به اطلاعاتی دقیق داریم. این نوع اطلاعات عموما توسط یک سری فایل مخصوص به نام Shapefiles که حاوی اطلاعات برداری جغرافیایی هستند ارائه می‌شوند. برای نمونه اطلاعات جغرافیایی به روز ایران را از آدرس ذیل می‌توانید دریافت کنید:
http://download.geofabrik.de/asia/iran.html
http://download.geofabrik.de/asia/iran-latest.shp.zip

پس از دریافت این فایل، به تعدادی فایل با پسوندهای shp، shx و dbf خواهیم رسید.
فایل‌های shp بیانگر فرمت اشکال ذخیره شده هستند. فایل‌های shx یک سری ایندکس بوده و فایل‌های dbf از نوع بانک اطلاعاتی dBase IV می‌باشند.
همچنین اگر فایل‌های prj را باز کنید، یک چنین اطلاعاتی در آن موجودند:
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
نکته‌ی مهمی که در اینجا باید مدنظر داشت، استاندارد GCS_WGS_1984 آن است. این استاندارد معادل است با استاندارد EPSG 4326. عدد 4326 آن جهت ثبت این اطلاعات در یک بانک اطلاعاتی SQL Server حائز اهمیت است (پارامتر coordinateSystemId در متد createPoint) و ممکن است از هر فایلی به فایل دیگر متفاوت باشد.



خواند‌ن فایل‌های shp در دات نت

پس از دریافت فایل‌های shp و بانک‌های اطلاعاتی مرتبط با اطلاعات جغرافیایی ایران، اکنون نوبت به پردازش این فایل‌های مخصوص با فرمت بانک اطلاعاتی فاکس پرو مانند، رسیده‌است. برای این منظور می‌توان از پروژه‌ی سورس باز ذیل استفاده کرد:

این پروژه در خواندن فایل‌های shp بدون نقص عمل می‌کند اما توانایی خواندن نام‌های فارسی وارد شده در این نوع بانک‌های اطلاعاتی را ندارد. برای رفع این مشکل، سورس آن را از Codeplex دریافت کنید. سپس فایل Shapefile.cs را گشوده و ابتدای خاصیت Current آن‌را به نحو ذیل تغییر دهید:
        /// <summary>
        /// Gets the current shape in the collection
        /// </summary>
        public Shape Current
        {
            get 
            {
                if (_disposed) throw new ObjectDisposedException("Shapefile");
                if (!_opened) throw new InvalidOperationException("Shapefile not open.");
               
                // get the metadata
                StringDictionary metadata = null;
                if (!RawMetadataOnly)
                {
                    metadata = new StringDictionary();
                    for (int i = 0; i < _dbReader.FieldCount; i++)
                    {
                        string value = _dbReader.GetValue(i).ToString();
                        if (_dbReader.GetDataTypeName(i) == "DBTYPE_WVARCHAR")
                        {
                            // برای نمایش متون فارسی نیاز است
                            value = Encoding.UTF8.GetString(Encoding.GetEncoding(720).GetBytes(value));
                        }
                        metadata.Add(_dbReader.GetName(i),
                            value);
                    }
                }
در اینجا فقط سطر استفاده از Encoding خاصی با شماره 720 و تبدیل آن به UTF8 اضافه شده‌است. پس از آن بدون مشکل می‌توان برچسب‌های فارسی را از فایل‌های dBase IV این نوع بانک‌های اطلاعاتی استخراج کرد (اصلاح شده‌ی آن در فایل پیوست مطلب موجود است).
using System.Collections.Generic;
using System.Linq;
using Catfood.Shapefile;

namespace EFGeoTests
{
    public class MapPoint
    {
        public Dictionary<string, string> Metadata { set; get; }
        public double X { set; get; }
        public double Y { set; get; }
    }

    public static class ShapeReader
    {
        public static IList<MapPoint> ReadShapeFile(string path)
        {
            var results = new List<MapPoint>();

            using (var shapefile = new Shapefile(path))
            {
                foreach (var shape in shapefile)
                {
                    if (shape.Type != ShapeType.Point)
                        continue;

                    var shapePoint = shape as ShapePoint;
                    if (shapePoint == null)
                        continue;


                     var metadataNames = shape.GetMetadataNames();
                    if(!metadataNames.Any())
                        continue;

                    var metadata = new Dictionary<string, string>();
                    foreach (var metadataName in metadataNames)
                    {
                        metadata.Add(metadataName,shape.GetMetadata(metadataName));
                    }

                    results.Add(new MapPoint
                    {
                        Metadata = metadata,
                        X = shapePoint.Point.X,
                        Y = shapePoint.Point.Y
                    });
                }
            }

            return results;
        }
    }
}
در کدهای فوق به کمک کتابخانه‌ی C# Esri Shapefile Reader، اطلاعات نقاط بانک اطلاعاتی shape files را خوانده و به صورت لیست‌هایی از MapPoint بازگشت می‌دهیم. نکته‌ی مهم آن، Metadata است که از هر فایلی به فایل دیگر می‌توان متفاوت باشد. به همین جهت این اطلاعات را به شکل ویژگی‌های key/value در این نوع بانک‌های اطلاعاتی ذخیره می‌کنند.


افزودن اطلاعات جغرافیایی به بانک اطلاعاتی SQL Server به کمک Entity framework

فایل places.shp را در مجموعه فایل‌هایی که در ابتدای بحث عنوان شدند، می‌توانید مشاهده کنید. قصد داریم اطلاعات نقاط آن‌را به مدل GeoLocation انتساب داده و سپس ذخیره کنیم:
            var points = ShapeReader.ReadShapeFile("IranShapeFiles\\places.shp");
            using (var context = new MyContext())
            {
                context.Configuration.AutoDetectChangesEnabled = false;
                context.Configuration.ProxyCreationEnabled = false;
                context.Configuration.ValidateOnSaveEnabled = false;

                if (context.GeoLocations.Any())
                    return;

                foreach (var point in points)
                {
                    context.GeoLocations.Add(new GeoLocation
                    {
                        Name = point.Metadata["name"],
                        Type = point.Metadata["type"],
                        Location = createPoint(point.X, point.Y)
                    });
                }

                context.SaveChanges();
            }
تعریف متد createPoint را که بر اساس X و Y نقاط، معادل قابل پذیرش آن‌را جهت SQL Server تهیه می‌کند، در ابتدای بحث مشاهده کردید.
در فایل‌های مرتبط با places.shp، متادیتا name، معادل نام شهرهای ایران است و type آن بیانگر شهر، روستا و امثال آن می‌باشد.
پس از اینکه اطلاعات مکان‌های ایران، در SQL Server ذخیره شدند، نمایش بصری آن‌ها را در management studio نیز می‌توان مشاهده کرد:



کوئری گرفتن از اطلاعات جغرافیایی

فرض کنید می‌خواهیم مکان‌هایی را با فاصله کمتر از 5 کیلومتر از تهران پیدا کنیم:
            var tehran = createPoint(51.4179604, 35.6884243);

            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var locations = context.GeoLocations
                    .Where(loc => loc.Location.Distance(tehran) < 5000)
                    .OrderBy(loc => loc.Location.Distance(tehran))
                    .ToList();

                foreach (var location in locations)
                {
                    Console.WriteLine(location.Name);
                }
            }
همانطور که پیشتر نیز عنوان شد، متد Distance بر اساس متر کار می‌کند. به همین جهت برای تعریف 5 کیلومتر به نحو فوق عمل شده‌است. همچنین نحوه‌ی مرتب سازی اطلاعات نیز بر اساس فاصله از یک مکان مشخص صورت گرفته‌است.
و یا اگر بخواهیم دقیقا بر اساس مختصات یک نقطه، مکانی را بیابیم، می‌توان از متد SpatialEquals استفاده کرد:
            var tehran = createPoint(51.4179604, 35.6884243);
            using (var context = new MyContext())
            {
                // find any locations within 5 kilometers ordered by distance
                var tehranLocation = context.GeoLocations.FirstOrDefault(loc => loc.Location.SpatialEquals(tehran));
                if (tehranLocation != null)
                {
                    Console.WriteLine(tehranLocation.Type);
                }
            }

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
EFGeoTests.zip