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

1- مقدمه

پارتیشن بندی در بانک اطلاعاتی SQL Server، از ویژگی‌هایی است که از نسخه 2005، به این محصول اضافه شده است. بکارگیری این قابلیت که با Split کردن، محتوای یک جدول و قرار دادن آنها در چندین فایل، برای جداول حجیم، به  ویژه جداولی که داده‌های آن حاوی مقادیر تاریخچه‌ای است، بسیار سودمند است.
سادگی در مدیریت داده‌ها و شاخص‌های موجود یک جدول (از قبیل اندازه  فضای ذخیره سازی و استراتژی جدید Back up گیری)، اجرای سریعتر کوئری هایی که روی یک محدوده از داده‌ها کار  می‌کنند و سهولت در آرشیو داده‌های قدیمی یک جدول، از قابلیت‌هایی است که استفاده از این ویژگی بوجود می‌آورد.
محدوده استفاده از این ویژگی روی یک بانک اطلاعاتی و در یک Instance است. بنابراین مباحث مرتبط با معماری Scalability را پوشش نمی‌دهد و صرفاً Solution ایی است که در یک Instance بانک اطلاعاتی استفاده می‌شود.

2- Data File و Filegroup

هر بانک اطلاعاتی در حالت پیش فرض، شامل یک فایل داده‌ای (MDF.) و یک فایل ثبت تراکنشی (LDF.) می‌باشد. می‌توان جهت ذخیره سطر‌های داده‌ای از فایل‌های بیشتری تحت نام فایل‌های ثانویه (NDF.) استفاده نمود. به همان طریق که در فایل سیستم، فایل‌ها به پوشه‌ها تخصیص داده می‌شوند، می‌توان Data File را به Filegroup تخصیص داد. چنانچه چندین Data File به یک Filegroup تخصیص داده شوند، داده‌ها در تمامی Data File‌ها به طریق Round-Robin توزیع می‌شوند.

3- Partition Function

مطابق با مقادیر تعریف شده در بدنه دستور، محدوده داده‌ای (پارتیشن‌ها) با استفاده از Partition Function ایجاد می‌شود. با در نظر گرفتن ستونی که به عنوان Partition Key انتخاب شده، این تابع یک Data Type را به عنوان ورودی دریافت می‌کند. در هنگام تعریف محدوده برای پارتیشن‌ها، به منظور مشخص کردن محدوده هر پارتیشن از Right و Left استفاده می‌شود.
Left نمایش دهنده‌ی حد بالای هر محدوده است و به طور مشابه، Right برای مشخص کردن حد پائین آن محدوده استفاده می‌شود. به منظور درک بهتر، به شکل زیر توجه نمائید:
 

همانطور که مشاهده می‌شود، همواره نیاز به یک Filegroup اضافه‌تری از آنچه مورد نظرتان در تعریف تابع است، می‌باشد. بنابراین اگر Function دارای n مقدار باشد، به n+1 مقدار برای Filegroup نیاز است.
همچنین هیچ محدودیتی برای اولین و آخرین بازه در نظر گرفته نمی‌شود. بنابراین جهت محدود کردن مقادیری که در این بازه‌ها قرار می‌گیرند، می‌توان از Check Constraint استفاده نمود.

3-1- Right or Left

یک سوال متداول اینکه از کدام مورد استفاده شود؟ در پاسخ باید گفت، به چگونگی تعریف پارتیشن هایتان وابسته است. مطابق شکل، تنها تفاوت  این دو، در نقاط مرزی هر یک از پارتیشن‌ها می‌باشد. در بیشتر اوقات هنگام کار با داده‌های عددی می‌توان از Left استفاده نمود و بطور مشابه هنگامیکه نوع داده‌ها از جنس زمان است، می‌توان از Right استفاده کرد.

4- Partition Schema 

گام بعدی پس از ایجاد Partition Function، تعریف Partition Schema است، که به منظور قرار گرفتن هر یک از پارتیشن‌های تعریف شده توسط Function در Filegroup‌های مناسب آن استفاده می‌شود.

5- Partition Table

گام پایانی ایجاد یک جدول، استفاده از Partition Schema است، که داده‌ها را با توجه به رویه درون Partition Function مورد استفاده، ذخیره می‌کند. همانطور که می‌دانید هنگام ایجاد یک جدول، می‌توان مکان ذخیره شدن آنرا مشخص نمود.

 Create Table <name> (…) ON …
دستور بعد از بخش ON، مشخص کننده مکان ذخیره جدول می‌باشد.

در هنگام ایجاد یک جدول، معمولاً جدول در Filegroup پیش فرض که PRIMARY است، قرار می‌گیرد. می‌توان با نوشتن نام Partition Schema و همچنین Partition Key که پیشتر ذکر آن رفت، بعد از بخش ON، برای جدول مشخص نمائیم که داده‌های آن به چه ترتیبی ذخیره شوند. ارتباط این سه به شرح زیر است:

توجه شود زمانیکه یک Primary Key Constraint به یک جدول اضافه می‌شود، یک Unique Clustered Index نیز همراه با آن ساخته می‌شود. چنانچه Primary Key شامل یک Clustered Index باشد، جدول با استفاده از این ستون (ستون‌های) شاخص ذخیره خواهد شد، در حالیکه اگر Primary Key شامل یک Non Clustered Index باشد، یک ساختار ذخیره-سازی اضافی ایجاد خواهد شد که داده‌های جدول در آن قرار خواهند گرفت.

6- Index & Data Alignment

به عنوان یک Best Practice هنگام ایجاد یک Partition Table به منظور پارتیشن بندی، از ساختار Aligned Index استفاده شود. بدین ترتیب که تعریف Index، شامل Partition Key (ستونی که معیاری برای پارتیشن بندی است) باشد. چنانچه این عمل انجام شود، داده‌های ذخیره شده مرتبط با هر پارتیشن متناظر با همان شاخص، در فایل داده‌ای (NDF.) ذخیره خواهند شد. از این رو چنانچه کوئری درخواست شده از جدول روی یک محدوده باشد

 Where [OrderDate] Between …
تنها از شاخص متناظر با این داده استفاده می‌شود. بدین ترتیب بکارگیری آن برای Execution Plan بسیار سودمند خواهد بود. همچنین می‌توان استراتژی بازیافت سودمندی با Back up گیری از Filegroup ایجاد کرد. هنگامی که Index‌ها به صورت Aligned هستند می‌توان در کسری از ثانیه، محتوای یک Partition را به یک جدول دیگر منتقل نمود (تنها با تغییر در Meta Data آن).

بدین ترتیب برای بهرمندی از این مزایا، استفاده از Aligned Index توصیه شده است.

7- Operations

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

7-1- Split Partition

به منظور ایجاد یک محدوده جدید به پارتیشن‌ها استفاده می‌شود. یک نکته مهم مادامی که عملیات انتقال داده‌ها به پارتیشن جدید انجام می‌گیرد، روی جدول یک قفل انحصاری قرار می‌گیرد و بدین ترتیب عملیات ممکن است زمانبر باشد.
به عنوان یک Best Practice همواره یک Partition خالی را Split نمائید و پس از آن اقدام به بارگذاری داده در آن نمائید.
به یاد داشته باشید پیش از انجام عملیات splitting روی Partition Function با تغییر در Partition Schema (و بکارگیری Next Used) مشخص نمائید چه محدوده‌ای در این Filegroup جدید قرار خواهد گرفت.

7-2- Merge Partition

به منظور ادغام پارتیشن‌ها استفاده می‌شود، چنانچه پارتیشن خالی نیست، برای عملیات ادغام مسائل Performance به علت اینکه در طول عملیات از Lock (قفل انحصاری) استفاده می‌شود، در نظر گرفته شود.

7-3- Switch Partition

چنانچه جدول و شاخص‌های آن به صورت Aligned هستند، می‌توانید از Switch in و Switch out استفاده نمائید. عملیات بدین ترتیب انجام می‌شود که بلافاصله محتوای یک پارتیشن یا جدول (Source) در یک پارتیشن خالی جدولی دیگر و یا یک جدول خالی (Target) قرار می‌گیرد. عملیات تنها روی Meta Data انجام می‌گیرد و هیچ داده ای منتقل نمی‌شود.
محدودیت‌های بکارگیری به شرح زیر است:
- جدول یا پارتیشن Target باید حتماً خالی باشد.
- جداول Source و Target حتماً باید در یک Filegroup یکسان قرار داشته باشند.
- جدول Source باید حاوی Aligned Index‌های مورد نیاز Target و همچنین مطابقت در Filegroup را دارا باشد.
- چنانچه Target به عنوان یک پارتیشن است، اگر Source جدول است بایست دارای یک Check Constraint باشد در غیر این صورت چنانچه یک پارتیشن است باید محدوده آن در محدوده Target قرار گیرد.

8- بررسی یک سناریوی نمونه

در ابتدا یک بانک اطلاعاتی را به طریق زیر ایجاد می‌کنیم:
این بانک مطابق تصویر، شامل 3 عدد فایل گروپ (FG1، FG2 و FG3) و 3 عدد دیتا فایل (P1، P2 و P3) می‌باشد. Filegroup پیش فرض Primary است، که چنانچه در تعریف جداول به نام Partition Schema و Partition Key مرتبط اشاره نشود، به طور پیش فرض در Filegroup موسوم به Primary قرار می‌گیرد. چنانچه چک باکس Default انتخاب شود، همانطور که قابل حدس زدن است، آن Filegroup در صورت مشخص نکردن نام Filegroup در تعریف جدول، به عنوان مکان ذخیره سازی انتخاب می‌شود. چک باکس Read Only نیز همانطور که از نامش پیداست، چنانچه روی یک Filegroup تنظیم گردد، عملیات مربوط به Write روی داده‌های آن قابل انجام نیست و برای Filegroup هایی که جنبه نگهداری آرشیو را دارند، قابل استفاده است.
چنانچه Filegroup ای را از حالت Read Only دوباره خارج کنیم، می‌توان عملیات Write را دوباره برای آن انجام داد.

پس از ایجاد بانک اطلاعاتی، گام بعدی ایجاد یک Partition Function و پس از آن یک Partition Schema است. همانطور که مشاهده می‌کنید در Partition Function از سه مقدار استفاده شده، بنابراین در Partition Schema باید از چهار Filegroup استفاده شود، که در مثال ما از Filegroup پیش فرض که Primary است، استفاده شده است.

USE [PartitionDB]
GO
CREATE PARTITION FUNCTION pfOrderDateRange(DATETIME)
AS
RANGE LEFT FOR VALUES ('2010/12/31','2011/12/31','2012/12/31')
GO
CREATE PARTITION SCHEME psOrderDateRange
AS
PARTITION pfOrderDateRange TO (FG1,FG2,FG3,[PRIMARY])
GO

پس از طی گام‌های قبل، به ایجاد یک جدول به صورت Aligned Index مبادرت ورزیده می‌شود.

CREATE TABLE Orders
(
OrderID INT IDENTITY(1,1) NOT NULL,
OrderDate DATETIME NOT NULL,
OrderFreight MONEY NULL,
ProductID INT NULL,
CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderID ASC, OrderDate ASC)
ON psOrderDateRange (OrderDate)
) ON psOrderDateRange (OrderDate)
GO

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

SET NOCOUNT ON
DECLARE @OrderDate DATETIME
DECLARE @X INT
SET @OrderDate = '2010/01/01'
SET @X = 0
WHILE @X < 300
BEGIN
INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID)
VALUES( @OrderDate + @X, @X + 10, @X)
 SET @X = @X + 1
END
GO
SET NOCOUNT ON
DECLARE @OrderDate DATETIME
DECLARE @X INT
SET @OrderDate = '2011/01/01'
SET @X = 0
WHILE @X < 300
BEGIN
INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID)
VALUES( @OrderDate + @X, @X + 10, @X)
 SET @X = @X + 1
END
GO
SET NOCOUNT ON
DECLARE @OrderDate DATETIME
DECLARE @X INT
SET @OrderDate = '2012/01/01'
SET @X = 0
WHILE @X < 300
BEGIN
INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID)
VALUES( @OrderDate + @X, @X + 10, @X)
 SET @X = @X + 1
END
GO

از طریق دستور Select زیر می‌توان نحوه توزیع داده‌ها را در جدول مشاهده کرد.

USE [PartitionDB]
GO
SELECT OBJECT_NAME(i.object_id) AS OBJECT_NAME,
p.partition_number, fg.NAME AS FILEGROUP_NAME, ROWS, au.total_pages,
CASE boundary_value_on_right
WHEN 1 THEN 'Less than'
ELSE 'Less or equal than' END AS 'Comparition',VALUE
FROM sys.partitions p JOIN sys.indexes i
ON p.object_id = i.object_id AND p.index_id = i.index_id
JOIN sys.partition_schemes ps ON ps.data_space_id = i.data_space_id
JOIN sys.partition_functions f ON f.function_id = ps.function_id
LEFT JOIN sys.partition_range_values rv
ON f.function_id = rv.function_id
AND p.partition_number = rv.boundary_id
JOIN sys.destination_data_spaces dds
ON dds.partition_scheme_id = ps.data_space_id
AND dds.destination_id = p.partition_number
JOIN sys.filegroups fg
ON dds.data_space_id = fg.data_space_id
JOIN (SELECT container_id, SUM(total_pages) AS total_pages
FROM sys.allocation_units
GROUP BY container_id) AS au
ON au.container_id = p.partition_id WHERE i.index_id < 2

خروجی دستور فوق به شرح زیر است:


در ادامه به ایجاد یک Filegroup جدید می‌پردازیم.

/* Query 2-3- Split a partition*/
-- Add FG4:
ALTER DATABASE PartitionDB ADD FILEGROUP FG4
Go
ALTER PARTITION SCHEME [psOrderDateRange] NEXT USED FG4
GO
ALTER PARTITION FUNCTION [pfOrderDateRange]() SPLIT RANGE('2013/12/31')
GO
-- Add Partition 4 (P4) to FG4:
GO
ALTER DATABASE PartitionDB ADD FILE
(
NAME = P4,
FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10_50.SQLEXPRESS\MSSQL\DATA\P4.NDF'
, SIZE = 1024KB , MAXSIZE = UNLIMITED, FILEGROWTH = 10%)
 TO FILEGROUP [FG4]
--
GO

و در ادامه به درج اطلاعاتی برای بررسی نحوه توزیع داده‌ها در Filegroup هایمان می‌پردازیم.

SET NOCOUNT ON
DECLARE @OrderDate DATETIME
DECLARE @X INT
SET @OrderDate = '2013/01/01'
SET @X = 0
WHILE @X < 300
BEGIN
INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID)
VALUES( @OrderDate + @X, @X + 10, @X)
 SET @X = @X + 1
END
GO
SET NOCOUNT ON
DECLARE @OrderDate DATETIME
DECLARE @X INT
SET @OrderDate = '2012/01/01'
SET @X = 0
WHILE @X < 300
BEGIN
INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID)
VALUES( @OrderDate + @X, @X + 10, @X)
 SET @X = @X + 1
END
GO
خروجی کار تا این مرحله به شکل زیر است:

جهت ادغام پارتیشن‌ها به طریق زیر عمل می‌شود:

/* Query 2-4- Merge Partitions */
ALTER PARTITION FUNCTION [pfOrderDateRange]() MERGE RANGE('2010/12/31')
Go
پس از اجرای دستور فوق خروجی به شکل زیر خواهد بود:

به منظور آرشیو نمودن اطلاعات به طریق زیر از Switch استفاده می‌کنیم. ابتدا یک جدول موقتی برای ذخیره رکوردهایی که قصد آرشیو آنها را داریم، ایجاد می‌کنیم. همانگونه که در تعریف جدول مشاهده می‌کنید، نام Filegroup ای که برای ساخت این جدول استفاده می‌شود، با Filegroup ای که قصد آرشیو اطلاعات آنرا داریم، یکسان است.
در ادامه می‌توان مثلاً با ایجاد یک Temporary Table به انتقال این اطلاعات بدون توجه به Filegroup آنها پرداخت.

/* Query 2-5- Switch Partitions */
USE [PartitionDB]
GO
CREATE TABLE [dbo].[Orders_Temp](
[OrderID] [int] IDENTITY(1,1) NOT NULL,
[OrderDate] [datetime] NOT NULL,
[OrderFreight] [money] NULL,
[ProductID] [int] NULL,
 CONSTRAINT [PK_OrdersTemp] PRIMARY KEY CLUSTERED ([OrderID] ASC,[OrderDate] ASC)ON FG2
) ON FG2
GO
USE [tempdb]
GO
CREATE TABLE [dbo].[Orders_Hist](
[OrderID] [int] NOT NULL,
[OrderDate] [datetime] NOT NULL,
[OrderFreight] [money] NULL,
[ProductID] [int] NULL,
 CONSTRAINT [PK_OrdersTemp] PRIMARY KEY CLUSTERED ([OrderID] ASC,[OrderDate] ASC)
)
GO
USE [PartitionDB]
GO
ALTER TABLE [dbo].[Orders] SWITCH PARTITION 1 TO [dbo].[Orders_Temp]
GO
INSERT INTO [tempdb].[dbo].[Orders_Hist]
SELECT * FROM  [dbo].[Orders_Temp]
GO
DROP TABLE [dbo].[Orders_Temp]
GO
SELECT * FROM [tempdb].[dbo].[Orders_Hist]
پس از اجرای کامل این دستورات، توزیع داده در بانک اطلاعاتی مثال مورد بررسی به شکل زیر است.


 

مطالب
کش کردن اطلاعات غیر پویا در ASP.Net - قسمت اول

در مورد افزونه YSlow افزونه Firebug فایرفاکس پیشتر صحبت شد. این افزونه پس از آنالیز یک سایت، پیشنهاداتی را نیز جهت بهبود سرعت، ارائه می‌دهد.





همانطور که در شکل بالا مشخص است، عناصری مانند css و js ، قسمت expires اشان (تاریخ منقضی شدن کش آن‌ها در سمت کلاینت) خالی است و پیشنهاد داده که به هر کدام از این عناصر، هدر مخصوص مشخص سازی مدت زمان کش شدن در سمت کلاینت اضافه شود.
ASP.Net در مورد کش کردن اطلاعات صفحات پویا به اندازه‌ی کافی امکانات در اختیار برنامه نویس قرار می‌دهد اما در مورد اضافه کردن این هدر جهت یک فایل css غیر پویا شاید نتوان مطلب خاصی را یافت.
در IIS7 امکانات ویژه‌ای برای این منظور در نظر گرفته شده که نحوه استفاده از آن در ASP.Net به صورت زیر است:
فایل وب کانفیگ سایت را باز کرده و به قسمت system.webServer چند سطر زیر را اضافه کنید:

<staticContent>
<clientCache httpExpires="Sun, 29 Mar 2020 00:00:00 GMT" cacheControlMode="UseExpires" />
</staticContent>

این مورد فقط مختص به IIS7 است و بر روی نگارش‌های پایین‌تر کار نمی‌کند.
با این کار، تاریخ منقضی شدن هر آنچه که توسط موتور ASP.net سرو نمی‌شود به سال 2020 تنظیم خواهد شد. (کلیه محتوای غیرپویای سایت، اعم از تصاویر، فایلهای css ، js و غیره)
پس از این تنظیم مجددا YSlow را اجرا کرده و Performance Grade ایی را که نمایش می‌دهد بررسی نمائید.

بدیهی است اگر یکی از فایل‌های css یا js شما تغییر کند، کلاینت، اطلاعات جدیدی را تا سال 2020 دریافت نمی‌کند. برای حل این مشکل یک کوئری استرینگ ساده به انتهای لینک مربوط به css‌ یا js‌ خود اضافه کنید تا URL جدید با URL قبلی آن یکسان نباشد (این کوئری استرینگ تاثیری روی محتوای ایستای ما ندارد). به این صورت این آدرس جدید، مجددا دریافت شده و تا سال 2020 کش خواهد شد.

نکته:
اعمال تنظیم فوق، در IIS7 ویندوز سرور 2008 مجاز است؛ اما در IIS7 ویندوز ویستا قفل شده است و قابل override نیست. برای تغییر آن، فایل زیر را پیدا کنید:
open %systemroot%\System32\inetsrv\config\applicationHost.config
و در آن سطر
<section name="staticContent" overrideModeDefault="Deny" />
را به صورت زیر تغییر دهید تا مجاز به اعمال تغییرات شوید:
<section name="staticContent" overrideModeDefault="Allow" />
در قسمت بعد در مورد نگارش‌های پایین‌تر IIS توضیح داده خواهد شد.


مآخذ:
YSlow: Add expires header to images in IIS 7
IIS7: How to set cache control for static content?

مطالب
ثبت لینک‌های غیرتکراری
ثبت لینک‌های مختلف در یک سیستم (مثلا قسمت به اشتراک گذاری لینک‌ها) در ابتدای کار شاید ساده به نظر برسد؛ خوب، هر صفحه‌ای که یک آدرس منحصربفرد بیشتر ندارد. ما هش این لینک را محاسبه می‌کنیم و بعد روی این هش، یک کلید منحصربفرد را تعریف خواهیم کرد تا دیگر رکوردی تکراری ثبت نشود. همچنین چون این هش نیز طول کوتاهی دارد، جستجوی آن بسیار سریع خواهد بود. واقعیت این است که خیر! این روش ناکارآمدترین حالت پردازش لینک‌های مختلف است.
برای مثال لینک‌های http://www.site.com و http://www.site.com/index.htm دو هش متفاوت را تولید می‌کنند اما در عمل یکی هستند. نمونه‌ی دیگر، لینک‌های http://www.site.com/index.htm و http://www.site.com/index.htm#section1 هستند که فقط اصطلاحا در یک fragment با هم تفاوت دارند و از این دست لینک‌هایی که باید در حین ثبت یکی درنظر گرفته شوند، زیاد هستند و اگر علاقمند به مرور آن‌ها هستید، می‌توانید به صفحه‌ی URL Normalization در ویکی‌پدیا مراجعه کنید.
اگر نکات این صفحه را تبدیل به یک کلاس کمکی کنیم، به کلاس ذیل خواهیم رسید:
using System;
using System.Web;

namespace OPMLCleaner
{
    public static class UrlNormalization
    {
        public static bool AreTheSameUrls(this string url1, string url2)
        {
            url1 = url1.NormalizeUrl();
            url2 = url2.NormalizeUrl();
            return url1.Equals(url2);
        }

        public static bool AreTheSameUrls(this Uri uri1, Uri uri2)
        {
            var url1 = uri1.NormalizeUrl();
            var url2 = uri2.NormalizeUrl();
            return url1.Equals(url2);
        }

        public static string[] DefaultDirectoryIndexes = new[]
            {
                "default.asp",
                "default.aspx",
                "index.htm",
                "index.html",
                "index.php"
            };

        public static string NormalizeUrl(this Uri uri)
        {
            var url = urlToLower(uri);
            url = limitProtocols(url);
            url = removeDefaultDirectoryIndexes(url);
            url = removeTheFragment(url);
            url = removeDuplicateSlashes(url);
            url = addWww(url);
            url = removeFeedburnerPart(url);
            return removeTrailingSlashAndEmptyQuery(url);
        }

        public static string NormalizeUrl(this string url)
        {
            return NormalizeUrl(new Uri(url));
        }

        private static string removeFeedburnerPart(string url)
        {
            var idx = url.IndexOf("utm_source=", StringComparison.Ordinal);
            return idx == -1 ? url : url.Substring(0, idx - 1);
        }

        private static string addWww(string url)
        {
            if (new Uri(url).Host.Split('.').Length == 2 && !url.Contains("://www."))
            {
                return url.Replace("://", "://www.");
            }
            return url;
        }

        private static string removeDuplicateSlashes(string url)
        {
            var path = new Uri(url).AbsolutePath;
            return path.Contains("//") ? url.Replace(path, path.Replace("//", "/")) : url;
        }

        private static string limitProtocols(string url)
        {
            return new Uri(url).Scheme == "https" ? url.Replace("https://", "http://") : url;
        }

        private static string removeTheFragment(string url)
        {
            var fragment = new Uri(url).Fragment;
            return string.IsNullOrWhiteSpace(fragment) ? url : url.Replace(fragment, string.Empty);
        }

        private static string urlToLower(Uri uri)
        {
            return HttpUtility.UrlDecode(uri.AbsoluteUri.ToLowerInvariant());
        }

        private static string removeTrailingSlashAndEmptyQuery(string url)
        {
            return url
                    .TrimEnd(new[] { '?' })
                    .TrimEnd(new[] { '/' });
        }

        private static string removeDefaultDirectoryIndexes(string url)
        {
            foreach (var index in DefaultDirectoryIndexes)
            {
                if (url.EndsWith(index))
                {
                    url = url.TrimEnd(index.ToCharArray());
                    break;
                }
            }
            return url;
        }
    }
}
از این روش برای تمیز کردن و حذف فیدهای تکراری در فایل‌های OPML تهیه شده نیز می‌شود استفاده کرد. عموما فیدخوان‌های نه‌چندان با سابقه، نکات یاد شده در این مطلب را رعایت نمی‌کنند و به سادگی می‌شود در این سیستم‌ها، فیدهای تکراری زیادی را ثبت کرد.
برای مثال اگر یک فایل OPML چنین ساختار XML ایی را داشته باشد:
<?xml version="1.0" encoding="utf-8"?>
<opml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1.0">
  <body>
    <outline text="آی تی ایرانی">
    <outline type="rss" 
                           text="‫فید کلی آخرین نظرات، مطالب، اشتراک‌ها و پروژه‌های .NET Tips" 
                           title="‫فید کلی آخرین نظرات، مطالب، اشتراک‌ها و پروژه‌های .NET Tips" 
                           xmlUrl="https://www.dntips.ir/Feed/LatestChanges"
                           htmlUrl="https://www.dntips.ir/" />
    </outline> 
  </body>
</opml>
هر outline آن‌را به کلاس زیر می‌توان نگاشت کرد:
using System.Xml.Serialization;

namespace OPMLCleaner
{
    [XmlType(TypeName="outline")]
    public class Opml
    {
        [XmlAttribute(AttributeName="text")]
        public string Text { get; set; }

        [XmlAttribute(AttributeName = "title")]
        public string Title { get; set; }

        [XmlAttribute(AttributeName = "type")]
        public string Type { get; set; }

        [XmlAttribute(AttributeName = "xmlUrl")]
        public string XmlUrl { get; set; }

        [XmlAttribute(AttributeName = "htmlUrl")]
        public string HtmlUrl { get; set; }
    }
}
برای اینکار فقط کافی است از LINQ to XML به نحو ذیل استفاده کنیم:
var document = XDocument.Load("it-92-03-01.opml");
var results = (from node in document.Descendants("outline")
                           where node.Attribute("htmlUrl") != null && node.Parent.Attribute("text") != null 
                            && node.Parent.Attribute("text").Value == "آی تی ایرانی"
                           select new Opml
                           {
                               HtmlUrl = (string)node.Attribute("htmlUrl"),
                               Text = (string)node.Attribute("text"),
                               Title = (string)node.Attribute("title"),
                               Type = (string)node.Attribute("type"),
                               XmlUrl = (string)node.Attribute("xmlUrl")
                           }).ToList();
در این حالت لیست کلیه فیدهای یک گروه را چه تکراری و غیرتکراری، دریافت خواهیم کرد. برای حذف موارد تکراری نیاز است از متد Distinct استفاده شود. به همین جهت باید کلاس ذیل را نیز تدارک دید:
using System.Collections.Generic;

namespace OPMLCleaner
{
    public class OpmlCompare : EqualityComparer<Opml>
    {
        public override bool Equals(Opml x, Opml y)
        {
            return UrlNormalization.AreTheSameUrls(x.HtmlUrl, y.HtmlUrl);                
        }

        public override int GetHashCode(Opml obj)
        {
            return obj.HtmlUrl.GetHashCode();
        }
    }
}
اکنون با کمک کلاس OpmlCompare فوق که از کلاس UrlNormalization برای تشخیص لینک‌های تکراری استفاده می‌کند، می‌توان به لیست بهتر و متعادل‌تری رسید:
 var distinctResults = results.Distinct(new OpmlCompare()).ToList();
 
بازخوردهای پروژه‌ها
عدم سازگاری با EF
با سلام و احترام
وقتی دیتا سورس از جنس اینتیتی تعریف می‌کنم بحتی هیچ ستونی هم اد نمی‌کنم بدون اینکه خطایی دریافت کنم فایل پی دی اف، read only  می‌مونه و حجم فایل 0kb باقی می‌مونه
 public static IPdfReportData CreatePdfReport(Order order, int languageId)
        {
            if (order == null)
                throw new ArgumentNullException("order");



            Language lang = IoC.Resolve<ILanguageService>().GetLanguageById(languageId);

            if (lang == null)
                throw new NopException("Language could not be loaded");

            var localizationManager = IoC.Resolve<ILocalizationManager>();
            var orderProductVariants = order.OrderProductVariants;
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "coponet", Application = "coponet eshop", Keywords = "Factor", Subject = "Factor", Title = "Factor" });
                doc.Compression(new CompressionSettings
                {
                    EnableCompression = true,
                    EnableFullCompression = true
                });
                doc.PrintingPreferences(new PrintingPreferences
                {
                    ShowPrintDialogAutomatically = false
                });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\arial.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(CalenderHelper.dateTimeParseToString("yyyy/MM/dd", order.CreatedOn, CalenderEnum.PersianCalender));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.RunDirection(PdfRunDirection.LeftToRight);
                    //defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("Our new rpt.");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.ClassicTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(1);
            })
            .MainTableDataSource(dataSource =>
            {
                //var listOfRows = new List<User>();
                //for (int i = 0; i < 40; i++)
                //{
                //    listOfRows.Add(new User {Id = i});
                //}
                //dataSource.StronglyTypedList(listOfRows);
                dataSource.StronglyTypedList(orderProductVariants);
            })
            )
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
           ).Generate(data => data.AsPdfFile(string.Format("{0}\\documents\\Temp\\Factor-{1}.pdf", HttpRuntime.AppDomainAppPath, order.OrderId)));
        }

این نمونه کدی که استفاده کردم

مطالب
آشنایی با Window Function ها در SQL Server بخش اول
Window Function‌ها برای اولین بار در نسخه SQL Server 2005 ارائه گردیدند، و در ورژن‌های جدیدتر SQL Server، به تعداد این فانکشنها افزوده شده است.

تعریف Window Function :
        معمولا از این نوع فانکشنها روی مجموعه ای از ROW‌های یک جدول، در جهت اعمال عملیاتهای محاسباتی ،ارزیابی داده ها، رتبه بندی و غیرو... استفاده می‌گردد، به بیان ساده‌تر بوسیله Window Function‌ها می‌توان، ROW‌های یک جدول را گروه بندی نمود. و روی گروه‌ها از توابع جمعی (Aggregate Functions ) استفاده کرد. این نوع فانکشنها از قابلیت و انعطاف پذیری زیادی برخوردار می‌باشند، و بوسیله آنها می‌توان نتایج (خروجی) بسیار مفیدی از Query ها، بدست آورد، معمولا از این نوع فانکشنها در            Data Mining (داده کاوی) و گزارشگیری‌ها استفاده می‌گردد. و آگاهی و روش استفاده از Window Function‌ها برای برنامه نویسان و DBA ها، می‌تواند بسیار مفید باشد.
مفهوم Window Function مطابق استاندارد ISO و ANSI می‌باشد، و دیتابیس هایی همچون Oracle،DB2،Sybase از آن پشتیبانی می‌نمایند.برای اطلاعات بیشتر می‌توانید به سایت‌های زیر مراجعه کنید:
کلمه "Window" در  Window Function، به مجموعه ROW هایی اشاره می‌کند، که محاسبات و ارزیابی و غیرو... روی آنها اعمال می‌گردد. 
Window Function‌ها برای ارائه قابلیت‌های خود، از Over Clause استفاده می‌کنند. اگر مقاله آشنایی با Row_Number،Rank،Dense_Rank،NTILE را مطالعه کرده باشید، می‌توان هریک از آنها را یک Window Function دانست.
برای شروع، به بررسی Over Clause می‌پردازیم، و Syntax آن به شرح ذیل می‌باشد:
OVER ( 
       [ <PARTITION BY clause> ]
       [ <ORDER BY clause> ] 
       [ <ROW or RANGE clause> ]
      )

<PARTITION BY clause> ::=
PARTITION BY value_expression , ... [ n ]

<ORDER BY clause> ::=
ORDER BY order_by_expression
    [ COLLATE collation_name ] 
    [ ASC | DESC ] 
    [ ,...n ]

<ROW or RANGE clause> ::=
{ ROWS | RANGE } <window frame extent>

<window frame extent> ::= 
{   <window frame preceding>
  | <window frame between>
}

<window frame between> ::= 
  BETWEEN <window frame bound> AND <window frame bound>

<window frame bound> ::= 
{   <window frame preceding>
  | <window frame following>
}

<window frame preceding> ::= 
{
    UNBOUNDED PRECEDING
  | <unsigned_value_specification> PRECEDING
  | CURRENT ROW
}

<window frame following> ::= 
{
    UNBOUNDED FOLLOWING
  | <unsigned_value_specification> FOLLOWING
  | CURRENT ROW
}

<unsigned value specification> ::= 
{  <unsigned integer literal> }
OVER دارای سه آرگومان اختیاری است که هر کدام را به تفصیل بررسی می‌کنیم:
1- PARTITION BY clause : بوسیله این پارامتر می‌توانیم Row‌های یک جدول را گروه بندی نماییم. این پارامتر یک  value_expression می پذیرد. یک Value_expression می‌تواند نام یک ستون ، یک Scalar Subquery ، Scalar Function و غیرو باشد.
2- ORDER BY clause : از نامش مشخص است و برای Sort استفاده می‌شود، و ویژگی‌های Order By در آن اعمال می‌گردد. به جز Offset.
3- ROW or RANGE clause :این پارامتر بیشتر برای محدود نمودن Row در یک Partition (گروه) مورد استفاده قرار می‌گیرد، به عنوان مثال نقطه شروع و پایان را می‌توان بوسیله پارامتر فوق تعیین نمود.
Row و Range نسبت به هم یک تفاوت عمده دارند،و آن این است که، اگر از ROW Clause استفاده نمایید، ارتباط ROW‌های قبلی یا بعدی، نسبت به Row جاری،بصورت فیزیکی (physical association ) سنجیده می‌شود، بطوریکه با استفاده از Range Clause ارتباط سطرهای قبلی و بعدی، نسبت به سطر جاری بصورت منطقی (logical association ) در نظر گرفته می‌شود. ممکن است درک این مطلب کمی سخت باشد، در ادامه با مثالهایی که بررسی می‌نماییم، براحتی تفاوت این دو را متوجه می‌شوید.
Row یا Range در قالب‌های متفاوتی مقدار می‌پذیرند، که هر کدام را بررسی می‌کنیم:
UNBOUNDED PRECEDING : بیانگر اولین سطر Partition می‌باشد. UNBOUNDED PRECEDING  فقط نقطه شروع را مشخص می‌نماید.
UNBOUNDED FOLLOWING : بیانگر آخرین سطر Partition می‌باشد. UNBOUNDED FOLLOWING فقط نقطه پایانی را مشخص می‌نماید.
CURRENT ROW : اولین سطر جاری یا آخرین سطر جاری را مشخص می‌نماید.
n PRECEDING یا unsigned value specification> PRECEDING> : تعداد سطر‌های قبل از سطر جاری را تعیین می‌کند، n یا <unsigned value specification>تعداد سطر‌های قبل از سطر جاری را تعیین می‌نماید. از n PRECEDING نمی توان برای Range استفاده نمود.  
n FOLLOWING یا unsigned value specification> FOLLOWING> : تعداد سطرهای بعد از سطر جاری را تعیین می‌کند، n یا<unsigned value specification> تعداد سطر  های بعد از سطر جاری را تعیین می‌نماید. از n FOLLOWING نمی توان برای Range استفاده نمود.
<BETWEEN <window frame bound > AND <window frame bound  : از چارچوب فوق برای Range و Row می‌توان استفاده نمود، و نقطه آغازین و نقطه پایانی توسط قالب فوق تعیین می‌گردد. نکته قابل توجه آن است که نقطه پایانی نمی‌تواند، کوچکتر از نقطه آغازین گردد.
در ادامه برای درک هرچه بیشتر تعاریف بیان شده، چندین مثال می‌زنیم و هر کدام را بررسی می‌نماییم:
در ابتدا Script زیر را اجرا نمایید، که شامل جدولی به نام Revenue (سود،درآمد) و درج چند درکورد در آن:
CREATE TABLE REVENUE
(
[DepartmentID] int,
[Revenue] int,
[Year] int
);
 
insert into REVENUE
values (1,10030,1998),(2,20000,1998),(3,40000,1998),
 (1,20000,1999),(2,60000,1999),(3,50000,1999),
 (1,40000,2000),(2,40000,2000),(3,60000,2000),
 (1,30000,2001),(2,30000,2001),(3,70000,2001)
 
مثال اول : می‌خواهیم براساس فیلد DepartmentID جدول Revenue را Partition بندی نماییم و از توابع جمعی AVG و SUM روی فیلد درآمد(Revenue) استفاده کنیم.
ابتدا Script زیر را اجرا می‌کنیم:
 select *,
 avg(Revenue) OVER (PARTITION by DepartmentID) as AverageRevenue,
 sum(Revenue) OVER (PARTITION by DepartmentID) as TotalRevenue
from REVENUE
order by departmentID, year;
خروجی بصورت زیر خواهد بود:

        مطابق شکل، جدول براساس  فیلد DepartmentID به سه Partition تقسیم شده است، و عملیات میانگین و جمع روی فیلد Revenue انجام شده است و عملیات Sort روی هرگروه بطور مستقل انجام گرفته است. چنین کاری را نمی‌توانستیم بوسیله Group By انجام دهیم.
مثال دوم : نحوه استفاده از ROWS PRECEDING،در این مثال قصد داریم عملیات جمع را روی فیلدRevenue انجام دهیم. بطوریکه جمع هر مقدار برابر است با سه مقدار قبلی + مقدار جاری:
 لطفا رکورد‌های زیر را به جدول فوق درج نمایید:
 insert into REVENUE
 values(1,90000,2002),(2,20000,2002),(3,80000,2002),
 (1,10300,2003),(2,1000,2003), (3,90000,2003),
 (1,10000,2004),(2,10000,2004),(3,10000,2004),
 (1,20000,2005),(2,20000,2005),(3,20000,2005),
 (1,40000,2006),(2,30000,2006),(3,30000,2006),
 (1,70000,2007),(2,40000,2007),(3,40000,2007),
 (1,50000,2008),(2,50000,2008),(3,50000,2008),
 (1,20000,2009),(2,60000,2009),(3,60000,2009),
 (1,30000,2010),(2,70000,2010),(3,70000,2010),
 (1,80000,2011),(2,80000,2011),(3,80000,2011),
 (1,10000,2012),(2,90000,2012),(3,90000,2012)
سپس Script زیر را اجرا می‌نماییم:
select Year, DepartmentID, Revenue,
sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR]
             ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3
From REVENUE order by departmentID, year;
خروجی :

        در Script بالا، جدول را براساس فیلد DepartmentID گروه بندی می‌کنیم، که سه گروه ایجاد می‌شود، هر گروه را بطور مستقل، روی فیلد Year بصورت صعودی مرتب می‌نماییم. حال برای آنکه بتوانیم سیاست جمع، روی فیلد Revenue، را پیاده سازی نماییم ، قطعه کد زیر را در Script بالا اضافه کردیم.
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3
     برای شرح چگونگی استفاده از PRECEDING،فقط به شرح گروه اول بسنده می‌کنیم. مقدار جمع فیلد Revenue سطر اول، که قبل از آن سطری وجود ندارد، برابر است با  مقدار خود، یعنی 10030، مقدار جمع فیلد Revenue سطر دوم برابر است با حاصل جمع مقدار فیلدRevenue سطر اول و دوم ، یعنی 30030 . این روند تا سطر چهار ادامه دارد، اما برای بدست آوردن مقدار جمع فیلدRevenue سطر پنجم، مقدار جمع فیلد Revenue سطر دوم،سوم،چهارم و پنجم در نظر گرفته می‌شود، و مقدار فیلدRevenue سطر اول در حاصل جمع در نظر گرفته نمی‌شود،بنابراین مقدار جمع فیلد Revenue سطر پنجم برابر است با 180000. در صورت مسئله گفته بودیم، مقدار جمع فیلد Revenue هر سطر جاری برابر است با حاصل جمع مقدارسطر جاری و مقادیر سه سطر ماقبل خود.

مثال سوم: نحوه استفاده از  ROWS FOLLOWING، این مثال عکس مثال دوم است، یعنی حاصل جمع مقدار فیلد Revenue هر سطر برابر است با حاصل جمع سطر جاری با سه سطر بعد از خود. بنابراین Script زیر را اجرا نمایید:
select Year, DepartmentID, Revenue,
 sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR]
              ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) as Next3
From REVENUE order by departmentID, year;
خروجی :

مطابق شکل مقدار جمع فیلد اول برابراست با حاصل جمع مقدار سطر جاری و سه سطر بعد از آن.
نکته ای که در مثالهای دوم و سوم،می بایست به آن توجه نمود، این است که در زمان استفاده از Row یا Range ، استفاده از Order by در Partition الزامی است، در غیر این صورت با خطا مواجه می‌شوید.


نحوه استفاده از UNBOUNDED PRECEDING ، این امکان در T-SQL Server 2012 افزوده شده است. 
مثال چهار: در این مثال می‌خواهیم کمترین سود بدست آمده در چند سال را بدست آوریم:
ابتدا Script زیر را اجرا نمایید:
select Year, DepartmentID, Revenue,
       min(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR]
                    ROWS UNBOUNDED PRECEDING) as MinRevenueToDate
From REVENUE order by departmentID, year;
خروجی:

طبق تعریف UNBOUNDED PRECEDING اولین سطر هر Partition را مشخص می‌نماید، و چون از PRECEDING استفاده کرده ایم، بنابراین مقایسه همیشه بین سطر جاری و  سطر‌های قبل از آن انجام می‌پذیرد. بنابراین خواهیم داشت، کمترین مقدار فیلد Revenue در سطر اول، برابر با مقدار خود می‌باشد، چون هیچ سطری ماقبل از آن وجود ندارد. در سطر دوم مقایسه کمترین مقدار، بین 20000 و 10030 انجام می‌گیرد، که برابر است با 10030، در سطر سوم، مقایسه بین مقادیر سطر اول،دوم و سطر سوم صورت می‌گیرد، یعنی کمترین مقدار بین 40000،20000 و 10030، بنابراین کمترین مقدار سطر سوم برابر است با 10030. 
به بیان ساده‌تر برای بدست آوردن کمترین مقدار هر سطر، مقدار سطر جاری با مقادیر همه سطرهای ماقبل خود مقایسه می‌گردد.
برای بدست آوردن کمترین مقدار در سطر ششم، مقایسه بین مقادیر سطر‌های اول،دوم،سوم،چهارم،پنجم و ششم صورت می‌گیرد که عدد 10000 بدست می‌آید و الی آخر...
نکنه: اگر در Over Clause شرط Order by را اعمال نماییم، اما از Row یا Range استفاده نکنیم، SQL Server بصورت پیش فرض از قالب زیر استفاده می‌نماید:
RANGE UNBOUNDED PRECEDING AND CURRENT ROW 
برای روشن‌تر شدن مطلب فوق مثالی می‌زنیم:
ابتدا Script زیر را اجرا نمایید، که شامل ایجاد یک جدول و درج چند رکورد در آن می‌باشد:
CREATE TABLE Employees (  
    EmployeeId INT IDENTITY PRIMARY KEY,  
    Name VARCHAR(50),  
    HireDate DATE NOT NULL,  
    Salary INT NOT NULL  
)  
GO  
INSERT INTO Employees (Name, HireDate, Salary)  
VALUES   
    ('Alice', '2011-01-01', 20000),  
    ('Brent', '2011-01-15', 19000),  
    ('Carlos', '2011-02-01', 22000),  
    ('Donna', '2011-03-01', 25000),  
    ('Evan', '2011-04-01', 18500)  
GO  
سپس Script زیر را اجرا نمایید:
SELECT  
    Name,   
    Salary,   
    AVG(Salary) OVER(ORDER BY HireDate) AS avgSalary  
FROM Employees  
GO 
خروجی :

حال اگر Script زیر را نیز اجرا نمایید، خروجی آن مطابق شکل بالا خواهد بود:
SELECT  
    Name,   
    Salary,   
    AVG(Salary) OVER(ORDER BY HireDate 
                 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS avgSalary  
FROM Employees  
GO
توضیح درباره Script بالا، در این روش برای بدست آوردن میانگین هر سطر، مقدار سطر جاری با مقادیر سطر‌های ماقبل خود جمع و تقسیم بر تعداد سطر می‌شود.
سطر دوم 20000 + 19000 تقسیم بر دو برابر است با 19500
میانگین سطر پنجم، حاصل جمع فیلد Salary همه مقادیر سطرها تقسیم بر 5 
*** اگر بخواهید بوسیله Over Clause ، میانگین همه سطر‌ها یکسان باشد می‌توانید از Script زیر استفاده نمایید:
SELECT  
    Name,   
    Salary,   
    AVG(Salary) OVER(ORDER BY HireDate   
                        RANGE   
                        BETWEEN UNBOUNDED PRECEDING   
                        AND UNBOUNDED FOLLOWING  
                    ) AS avgSalary  
FROM Employees  
GO  
خروجی :

منظور از  ROWS BETWEEN  UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING  یعنی در محاسبه میانگین برای هر سطر تمامی مقادیر سطر‌های دیگر در نظر گرفته شود.
پایان بخش اول
امیدوارم مفید واقع شده باشد.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 16 - کار با Sessions
در نگارش دو با این تنظیمات سشن 
app.UseSession();

services.AddSession(options =>
            {
                options.Cookie.Name = ".mySite";
                options.IdleTimeout = TimeSpan.FromMinutes(30);
           });
 بعد از چند پیام متوالی  warning به این شرح 
2017-10-30 14:46:15.846 +03:30 [Warning] Error unprotecting the session cookie.
System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
   at System.Convert.FromBase64_Decode(Char* startInputPtr, Int32 inputLength, Byte* startDestPtr, Int32 destLength)
   at System.Convert.FromBase64CharPtr(Char* inputPtr, Int32 inputLength)
   at System.Convert.FromBase64String(String s)
   at Microsoft.AspNetCore.Session.CookieProtection.Unprotect(IDataProtector protector, String protectedText, ILogger logger)
 با پیغام خطا بستن سشن  مواجه میشم.
2017-10-30 14:46:16.169 +03:30 [Error] Error closing the session.
System.OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at Microsoft.AspNetCore.Session.DistributedSession.<CommitAsync>d__32.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Session.SessionMiddleware.<Invoke>d__9.MoveNext()
مشکلی در اجرای برنامه به وجود نمیاد ولی دلیل پیام خطا مربوط چیه؟  
از سشن جهت دریافت Captcha استفاده شده. 
مطالب
کوئری نویسی در EF Core - قسمت پنجم - اعمال تجمعی - بخش اول
امکان انجام محاسبات تجمعی، یکی از مواردی است که قدرت بانک‌های اطلاعاتی رابطه‌ای را نمایش می‌دهد. توسط این نوع کوئری‌ها از محدوده‌ی CRUD (ثبت/ویرایش/به روز رسانی) خارج شده و وارد دنیای تصمیم‌گیری‌ها می‌شویم. تعداد مثال‌های اعمال تجمعی این سری قابل توجه‌است. به همین جهت در دو قسمت ارائه می‌شوند.


مثال 1: چه تعداد امکانات، توسط این مجموعه ارائه می‌شود؟
var count = context.Facilities.Count();
برای شمارش ساده‌ی تعداد ردیف‌های یک کوئری، از متد Count استفاده می‌شود که به صورت زیر ترجمه خواهد شد:



مثال 2: چه تعداد امکانات گران قیمتی توسط این مجموعه ارائه می‌شود؟
می‌خواهیم تعداد امکاناتی را بیابیم که guestcost آن‌ها بزرگتر یا مساوی 10 است.
var count = context.Facilities.Count(x => x.GuestCost >= 10);
این گزارش نسبت به مثال قبلی، یک Where را بیشتر دارد که می‌توان این شرط را در همان متد Count نیز ذکر کرد؛ با این خروجی نهایی:



مثال 3: هر کاربر چه تعدادی کاربر دیگری را توصیه کرده‌است؟
خروجی این گزارش بر اساس recommendedby و count باشد و مرتب شده‌ی بر اساس ID افراد.
var members = context.Members
                        .Where(member => member.RecommendedBy != null)
                        .GroupBy(member => member.RecommendedBy)
                        .Select(group => new
                        {
                            RecommendedBy = group.Key,
                            Count = group.Count()
                        })
                        .OrderBy(result => result.RecommendedBy)
                        .ToList();
در دو مثال قبل، تنها یک حاصل عددی را از گزارشات دریافت کردیم. اما در اینجا نیاز است به ازای هر شخص، تعداد توصیه شده‌های او محاسبه شوند. به همین جهت نیاز است اطلاعات را به ازای هر شخص مجزا، گروه بندی کرد و سپس متد Count را بر روی این گروه ویژه اعمال نمود و همینطور برای مابقی گروه‌ها. در اینجا GroupBy بر روی خاصیت RecommendedBy انجام شده‌است. این خاصیت در Select بعدی، همان group.Key استفاده شده‌است.



مثال 4: تعداد slots رزرو شد‌ه‌ی به ازای هر کدام از امکانات موجود را نمایش دهید.
جهت یادآوری از قسمت اول: «هر رزرو کردن مکان و امکاناتی در این مجموعه، «نیم ساعته» است. بنابراین Slots در اینجا به معنای تعداد نیم ساعت‌های رزرو کردن یک مکان خاص است؛ که به آن «half hour slots» نیز گفته می‌شود و زمان شروع این رزرو نیز ثبت می‌شود.»
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شده‌ی بر اساس ID هر امکان موجود.
var facilities = context.Bookings
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderBy(result => result.FacId)
                                    .ToList();
در این گزارش بجای استفاده از متد Count، از متد Sum استفاده شده‌است. چون می‌خواهیم جمع slots را به ازای هر امکان موجود محاسبه کنیم، ابتدا گروه‌های مجزای این امکانات را تشکیل می‌دهیم و سپس Sum را به هر گروهی، به صورت مجزایی اعمال می‌کنیم. در اینجا group.Key دقیقا همان booking.FacId است و در متد Sum، امکان دسترسی به خواص booking نیز وجود دارد.



مثال 5: تعداد slots رزرو شد‌ه‌ی در ماه September 2012 را به ازای هر کدام از امکانات موجود، نمایش دهید.
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شده‌ی بر اساس ID تعداد slots.
var date1 = new DateTime(2012, 09, 01);
var date2 = new DateTime(2012, 10, 01);

var facilities = context.Bookings
                                    .Where(booking => booking.StartTime >= date1
                                                        && booking.StartTime < date2)
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderBy(result => result.TotalSlots)
                                    .ToList();
این گزارش تنها یک قسمت Where را نسبت به گزارش قبلی بیشتر دارد و این Where باید دقیقا پیش‌از گروه بندی اطلاعات اعمال شود. به عبارتی ابتدا باید ردیف‌های اصلی گزارش مشخص باشند تا بتوان آن‌ها را گروه بندی کرد.



مثال 6: محاسبه کنید در سال 2012 و به ازای هر ماه مجزای آن، چه تعداد slots رزرو شده‌اند.
خروجی این گزارش بر اساس facid, month, Total Slots باشد و مرتب شده‌ی بر اساس ID و شماره‌ی ماه.
var date1 = new DateTime(2012, 01, 01);
var date2 = new DateTime(2013, 01, 01);

var facilities = context.Bookings
                                    .Where(booking => booking.StartTime >= date1
                                                        && booking.StartTime < date2)
                                    .GroupBy(booking => new { booking.FacId, booking.StartTime.Month })
                                    .Select(group => new
                                    {
                                        group.Key.FacId,
                                        group.Key.Month,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderBy(result => result.FacId)
                                        .ThenBy(result => result.Month)
                                    .ToList();
دو نکته‌ی جدید در این گزارش نسبت به مثال‌های قبلی وجود دارند:
الف) می‌توان گروه بندی را بر روی بیش از یک ستون اعمال کرد. در این حالت در Select بعدی، group.Key به کل شیء گروه بندی شده‌، اشاره می‌کند.
ب) روش انتخاب ماه میلادی از یک خاصیت DateTime و گروه بندی بر اساس آن

که به صورت زیر ترجمه می‌شود:



مثال 7: چه تعداد کاربر مجموعه، حداقل یکبار امکاناتی را رزرو کرده‌اند؟

var count = context.Bookings.Select(booking => booking.MemId).Distinct().Count();
هدف از این گزارش، رسیدن به COUNT DISTINCT است.
- (*)COUNT یعنی بازگشت تعداد ردیف‌های نهایی گزارش.
- COUNT(address) یعنی بازگشت تعداد آدرس‌های غیرنال، در کل ردیف‌های نهایی گزارش.
- COUNT(DISTINCT address) یعنی بازگشت تعداد آدرس‌های غیرمشابه در کل ردیف‌های نهایی گزارش.

  COUNT DISTINCT را EF-Core به صورت ترکیبی از یک sub-query ترجمه می‌کند:



مثال 8: امکاناتی را لیست کنید که بیش از 1000 slots رزرو شده دارند.
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شده‌ی بر اساس ID هر امکان موجود.
var facilities = context.Bookings
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .Where(result => result.TotalSlots > 1000)
                                    .OrderBy(result => result.FacId)
                                    .ToList();
در مثال‌های قبل، از Where جهت تشکیل تعداد ردیف‌های اصلی گزارش و سپس گروه بندی آن‌ها استفاده کردیم. در اینجا می‌خواهیم Where را بر روی نتیجه‌ی حاصل از گروه بندی اعمال کنیم. در کوئری LINQ فوق، خواص قابل دسترسی پس از Select نهایی، همان‌هایی هستند که توسط آن ارائه می‌شوند. این نوع Whereها در SQL حاصل به Having ترجمه خواهند شد:



مثال 9: میزان فروش کل هر امکان موجود را محاسبه کنید.
خروجی این گزارش بر اساس name, revenue باشد و مرتب شده‌ی بر اساس میزان فروش. بخاطر داشته باشید که میزان فروش کاربران ثبت نام شده با کاربران مهمان یکی نیست.
 var facilities =
                            context.Bookings.Select(booking =>
                                new
                                {
                                    booking.Facility.Name,
                                    Revenue = booking.MemId == 0 ?
                                            booking.Slots * booking.Facility.GuestCost
                                            : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => b.Name)
                                .Select(group => new
                                {
                                    Name = group.Key,
                                    TotalRevenue = group.Sum(b => b.Revenue)
                                })
                                .OrderBy(result => result.TotalRevenue)
                                .ToList();
همانند تمام گروه بندی‌ها، ابتدا باید ردیف‌های اصلی گزارش را تشکیل داد و سپس بر روی آن‌ها گروه بندی نهایی را اعمال نمود. به همین جهت در ابتدا خاصیت محاسباتی Revenue را بر اساس کاربران مهمان با ID مساوی صفر و کاربران اصلی مجموعه، تشکیل داده و گروه بندی را به نام هر مجموعه اعمال می‌کنیم. سپس جمع Revenue محاسبه شده را به ازای هر گروه محاسبه کرده و نتیجه را بازگشت می‌دهیم.



مثال 10: کدامیک از امکانات موجود، میزان فروشی کمتر از 1000 داشته‌اند؟
خروجی این گزارش بر اساس name, revenue باشد و مرتب شده‌ی بر اساس میزان فروش. بخاطر داشته باشید که میزان فروش کاربران ثبت نام شده با کاربران مهمان یکی نیست.
var facilities =
                            context.Bookings.Select(booking =>
                                new
                                {
                                    booking.Facility.Name,
                                    Revenue = booking.MemId == 0 ?
                                            booking.Slots * booking.Facility.GuestCost
                                            : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => b.Name)
                                .Select(group => new
                                {
                                    Name = group.Key,
                                    TotalRevenue = group.Sum(b => b.Revenue)
                                })
                                .Where(result => result.TotalRevenue < 1000)
                                .OrderBy(result => result.TotalRevenue)
                                .ToList();
این مورد نیز همانند گزارش 9 است که یک Where به نتیجه‌ی حاصل از آن اعمال شده که در خروجی نهایی به Having ترجمه می‌شود:



مثال 11: کدامیک از امکانات موجود، بیشترین slots رزرو شده را دارد؟

var item = context.Bookings
                                    .GroupBy(booking => booking.FacId)
                                    .Select(group => new
                                    {
                                        FacId = group.Key,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderByDescending(result => result.TotalSlots)
                                    .FirstOrDefault();
ساده‌ترین روش حل این مساله، گروه بندی اطلاعات بر اساس هر امکان موجود و سپس محاسبه‌ی TotalSlots هرکدام، به صورت مجزایی است. در ادامه ردیف‌های حاصل را بر اساس TotalSlots محاسبه شده، به صورت نزولی مرتب می‌کنیم. اولین ردیفی که در بالای گزارش قرار می‌گیرد همان FacId ای است که بیشترین TotalSlots را دارد.



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
مطالب
Blazor 5x - قسمت 29 - برنامه‌ی Blazor WASM - یک تمرین: رزرو کردن یک اتاق انتخابی


در قسمت قبل که لیست اتاق‌های دریافتی از Web API را نمایش دادیم، هرکدام از آن‌ها، به همراه یک دکمه‌ی Book هم هستند (تصویر فوق) که هدف از آن، فراهم آوردن امکان رزرو کردن آن اتاق، توسط کاربران سایت است. این قسمت را می‌توان به عنوان تمرینی جهت یادآوری مراحل مختلف تهیه‌ی یک Web API و قسمت‌های سمت کلاینت آن، تکمیل کرد.



تهیه موجودیت و مدل متناظر با صفحه‌ی ثبت رزرو یک اتاق

تا اینجا در برنامه‌ی سمت کلاینت، زمانیکه بر روی دکمه‌ی Go صفحه‌ی اول کلیک می‌کنیم، تاریخ شروع رزرو و تعداد روز مدنظر، به صفحه‌ی مشاهده‌ی لیست اتاق‌ها ارسال می‌شود. اکنون می‌خواهیم در این لیست اتاق‌های نمایش داده شده، اگر بر روی لینک Book اتاقی کلیک شد، به صفحه‌ی اختصاصی رزرو آن اتاق هدایت شویم (مانند تصویر فوق). به همین جهت نیاز است موجودیت متناظر با اطلاعاتی را که قرار است از کاربر دریافت کنیم، به صورت زیر به پروژه‌ی BlazorServer.Entities اضافه کنیم:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        public DateTime CheckInDate { get; set; }

        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        public long TotalCost { get; set; }

        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        [ForeignKey("RoomId")]
        public HotelRoom HotelRoom { get; set; }

        public string Status { get; set; }
    }
}
در اینجا مشخصات شروع و پایان رزرو یک اتاق مشخص و مشخصات کاربری که قرار است این فرم را پر کند، مشاهده می‌کنید که Status یا وضعیت آن، در پروژه‌ی مشترک BlazorServer.Common به صورت زیر تعریف می‌شود:
namespace BlazorServer.Common
{
    public static class BookingStatus
    {
        public const string Pending = "Pending";
        public const string Booked = "Booked";
        public const string CheckedIn = "CheckedIn";
        public const string CheckedOutCompleted = "CheckedOut";
        public const string NoShow = "NoShow";
        public const string Cancelled = "Cancelled";
    }
}
پس از این تعاریف، DbSet آن‌را نیز به ApplicationDbContext اضافه می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<RoomOrderDetail> RoomOrderDetails { get; set; }

        // ...
    }
}
بنابراین مرحله‌ی بعدی، ایجاد و اجرای Migrations متناظر با این جدول جدید است. برای این منظور با استفاده از خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddRoomOrderDetails --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
این دستورات به پروژه‌ی آغازین WebApi اشاره می‌کنند که قرار است از طریق سرویسی، با بانک اطلاعاتی ارتباط برقرار کند.

پس از تعریف یک موجودیت، یک DTO متناظر با آن‌را که جهت مدلسازی UI از آن استفاده خواهیم کرد، در پروژه‌ی BlazorServer.Models ایجاد می‌کنیم:
using System;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class RoomOrderDetailsDTO
    {
        public int Id { get; set; }

        [Required]
        public string UserId { get; set; }

        [Required]
        public string StripeSessionId { get; set; }

        [Required]
        public DateTime CheckInDate { get; set; }

        [Required]
        public DateTime CheckOutDate { get; set; }

        public DateTime ActualCheckInDate { get; set; }

        public DateTime ActualCheckOutDate { get; set; }

        [Required]
        public long TotalCost { get; set; }

        [Required]
        public int RoomId { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Email { get; set; }

        public string Phone { get; set; }

        public HotelRoomDTO HotelRoomDTO { get; set; }

        public string Status { get; set; }
    }
}
و همچنین در پروژه‌ی BlazorServer.Models.Mappings، نگاشت دوطرفه‌ی AutoMapper آن‌را نیز برقرار می‌کنیم؛ تا در حین تبدیل اطلاعات بین این دو، نیازی به تکرار سطرهای مقدار دهی اطلاعات خواص، نباشد:
namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ... 
            CreateMap<RoomOrderDetail, RoomOrderDetailsDTO>().ReverseMap(); // two-way mapping
        }
    }
}


ایجاد سرویسی برای کار با جدول RoomOrderDetails

در برنامه‌ی سمت کلاینت برای کار با بانک اطلاعاتی، دیگر نمی‌توان از سرویس‌های سمت سرور به صورت مستقیم استفاده کرد. به همین جهت آن‌ها را از طریق یک Web API endpoint، در معرض دید استفاده کننده قرار می‌دهیم. اما پیش از اینکار، سرویس سمت سرور Web API باید بتواند با سرویس دسترسی به اطلاعات جدول RoomOrderDetails، کار کند. بنابراین در ادامه این سرویس را تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details);

        Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync();

        Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId);

        Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate);

        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id);

        Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status);
    }
}
که به صورت زیر پیاده سازی می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public RoomOrderDetailsService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
        {
            var roomOrder = _mapper.Map<RoomOrderDetail>(details);
            roomOrder.Status = BookingStatus.Pending;
            var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
        }

        public Task<List<RoomOrderDetailsDTO>> GetAllRoomOrderDetailsAsync()
        {
            return _dbContext.RoomOrderDetails
                            .Include(roomOrderDetail => roomOrderDetail.HotelRoom)
                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                            .ToListAsync();
        }

        public async Task<RoomOrderDetailsDTO> GetRoomOrderDetailAsync(int roomOrderId)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.Id == roomOrderId);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }

        public Task<bool> IsRoomBookedAsync(int RoomId, DateTime checkInDate, DateTime checkOutDate)
        {
            return _dbContext.RoomOrderDetails
                .AnyAsync(
                    roomOrderDetail =>
                        roomOrderDetail.RoomId == RoomId &&
                        roomOrderDetail.IsPaymentSuccessful &&
                        (
                            (checkInDate < roomOrderDetail.CheckOutDate && checkInDate > roomOrderDetail.CheckInDate) ||
                            (checkOutDate > roomOrderDetail.CheckInDate && checkInDate < roomOrderDetail.CheckInDate)
                        )
                );
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(int id)
        {
            throw new NotImplementedException();
        }

        public Task<bool> UpdateOrderStatusAsync(int RoomOrderId, string status)
        {
            throw new NotImplementedException();
        }
    }
}
توضیحات:
- از متد CreateAsync برای تبدیل مدل فرم ثبت اطلاعات، به یک رکورد جدول RoomOrderDetails، استفاده می‌کنیم.
- متد GetAllRoomOrderDetailsAsync، لیست تمام سفارش‌های ثبت شده را بازگشت می‌دهد.
- متد GetRoomOrderDetailAsync بر اساس شماره اتاقی که دریافت می‌کند، لیست سفارشات آن اتاق خاص را بازگشت می‌دهد. این لیست به علت استفاده از Include‌های تعریف شده، به همراه مشخصات اتاق و همچنین تصاویر مرتبط با آن اتاق نیز هست.
- متد IsRoomBookedAsync بر اساس شماره اتاق و بازه‌ی زمانی درخواستی توسط یک کاربر مشخص می‌کند که آیا اتاق خالی شده‌است یا خیر؟

پس از تعریف این سرویس، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به سیستم تزریق وابستگی‌ها، معرفی می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IRoomOrderDetailsService, RoomOrderDetailsService>();
            // ...
 
 
تشکیل سرویس ابتدایی کار با RoomOrderDetails در پروژه‌ی WASM

در ادامه، تعاریف خالی سرویس سمت کلاینت کار با RoomOrderDetails  را به پروژه‌ی WASM اضافه می‌کنیم. تکمیل این سرویس را به قسمت بعدی واگذار خواهیم کرد:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
    }
}
با این پیاده سازی ابتدایی:
namespace BlazorWasm.Client.Services
{
    public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
    {
        private readonly HttpClient _httpClient;

        public ClientRoomOrderDetailsService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }

        public Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            throw new NotImplementedException();
        }
    }
}
که این مورد نیز باید به نحو زیر به سیستم تزریق وابستگی‌های برنامه‌ی سمت کلاینت در فایل Program.cs آن اضافه شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
            // ...
        }
    }
}


تعریف مدل فرم ثبت اطلاعات سفارش

پس از تدارک مقدمات فوق، اکنون می‌توانیم کار تکمیل فرم ثبت اطلاعات سفارش را شروع کنیم. به همین جهت مدل مخصوص آن‌را در برنامه‌ی سمت کلاینت به صورت زیر تشکیل می‌دهیم:
using BlazorServer.Models;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HotelRoomBookingVM
    {
        public RoomOrderDetailsDTO OrderDetails { get; set; }
    }
}


تعریف کامپوننت جدید RoomDetails و مقدار دهی اولیه‌ی مدل آن

در ادامه فایل جدید BlazorWasm.Client\Pages\HotelRooms\RoomDetails.razor را ایجاد کرده و به صورت زیر مقدار دهی اولیه می‌کنیم:
@page "/hotel/room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService

@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{

}

@code {
    [Parameter]
    public int? Id { get; set; }

    HotelRoomBookingVM HotelBooking  = new HotelRoomBookingVM();
    int NoOfNights = 1;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                if (await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking) != null)
                {
                    var roomInitialInfo = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, roomInitialInfo.StartDate, roomInitialInfo.EndDate);
                    NoOfNights = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.CheckInDate = roomInitialInfo.StartDate;
                    HotelBooking.OrderDetails.CheckOutDate = roomInitialInfo.EndDate;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = roomInitialInfo.NoOfNights;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        roomInitialInfo.NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
                else
                {
                    HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
                        Id.Value, DateTime.Now, DateTime.Now.AddDays(1));
                    NoOfNights = 1;
                    HotelBooking.OrderDetails.CheckInDate = DateTime.Now;
                    HotelBooking.OrderDetails.CheckOutDate = DateTime.Now.AddDays(1);
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = 1;
                    HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                        HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
- در ابتدا مسیریابی کامپوننت جدید RoomDetails را مشخص کرد‌ه‌ایم که یک Id را می‌پذیرد که همان Id اتاق انتخاب شده‌ی توسط کاربر است. به همین جهت پارامتر عمومی متناظری با آن هم در قسمت کدهای کامپوننت تعریف شده‌است.
- سپس سرویس توکار IJSRuntime به کامپوننت تزریق شده‌است تا توسط آن و Toastr، بتوان خطاهایی را به کاربر نمایش داد.
- از سرویس ILocalStorageService برای دسترسی به اطلاعات شروع به رزرو شخص و تعداد روز مدنظر او استفاده می‌کنیم که در قسمت قبل آن‌را مقدار دهی کردیم.
- همچنین از سرویس IClientHotelRoomService که آن‌را نیز در قسمت قبل افزودیم، برای فراخوانی متد GetHotelRoomDetailsAsync آن استفاده کرده‌ایم.

در روال آغازین OnInitializedAsync، اگر Id تنظیم شده بود، یعنی کاربر به درستی وارد این صفحه شده‌است. سپس بررسی می‌کنیم که آیا اطلاعاتی از درخواست ابتدایی او در Local Storage مرورگر وجود دارد یا خیر؟ اگر این اطلاعات وجود داشته باشد، بر اساس آن، بازه‌ی تاریخی دقیقی را می‌توان تشکیل داد و اگر خیر، این بازه را از امروز، به مدت 1 روز درنظر می‌گیریم.
پس از پایان کار متد OnInitializedAsync، چون اجزای HotelBooking مقدار دهی کامل شده‌اند، نمایش loading ابتدای کامپوننت، متوقف شده و قسمت else شرط نوشته شده اجرا می‌شود؛ یعنی اصل UI فرم نمایان خواهد شد.

در قسمت قبل، متد GetHotelRoomDetailsAsync را تکمیل نکردیم؛ چون به آن نیازی نداشتیم و فقط قصد داشتیم تا لیست تمام اتاق‌ها را نمایش دهیم. اما در اینجا برای تکمیل کدهای آغازین کامپوننت RoomDetails، متد دریافت اطلاعات یک اتاق را نیز تکمیل می‌کنیم تا توسط آن بتوان در این کامپوننت نیز جزئیات اتاق انتخابی را نمایش داد:
namespace BlazorWasm.Client.Services
{
    public class ClientHotelRoomService : IClientHotelRoomService
    {
        private readonly HttpClient _httpClient;

        public ClientHotelRoomService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/hotelroom/{roomId}"))
                            .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                            .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                            .Uri;
            return _httpClient.GetFromJsonAsync<HotelRoomDTO>(uri);
        }

        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
        {
           // ...
        }
    }
}

اتصال مدل کامپوننت RoomDetails به فرم ثبت سفارش آن

تا اینجا مدل فرم را مقدار دهی اولیه کردیم. اکنون می‌توانیم قسمت else شرط نوشته شده را تکمیل کرده و در قسمتی از آن، مشخصات اتاق جاری را نمایش دهیم و در قسمتی دیگر، فرم ثبت سفارش را تکمیل کنیم.
الف) نمایش مشخصات اتاق جاری
در کامپوننت جاری با استفاده از خواص مقدار دهی اولیه شده‌ی شیء HotelBooking.OrderDetails.HotelRoomDTO، می‌توان جزئیات اتاق انتخابی را نمایش داد که نمونه‌ای از آن‌را در قسمت قبل هم مشاهده کردید:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
    <div class="mt-4 mx-4 px-0 px-md-5 mx-md-5">
        <div class="row p-2 my-3 " style="border-radius:20px; ">
            <div class="col-12 col-lg-7 p-4" style="border: 1px solid gray">
                <div class="row px-2 text-success border-bottom">
                    <div class="col-8 py-1"><p style="font-size:x-large;margin:0px;">Selected Room</p></div>
                    <div class="col-4 p-0"><a href="hotel/rooms" class="btn btn-secondary btn-block">Back to Room's</a></div>
                </div>
                <div class="row">
                    <div class="col-6">
                        <div id="" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel">
                            <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel">
                                <ol class="carousel-indicators">
                                    <li data-target="#carouselExampleIndicators" data-slide-to="0" class="active"></li>
                                    <li data-target="#carouselExampleIndicators" data-slide-to="1"></li>
                                </ol>
                                <div class="carousel-inner">
                                    <div class="carousel-item active">
                                        <img class="d-block w-100" src="images/slide1.jpg" alt="First slide">
                                    </div>
                                </div>
                                <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev">
                                    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Previous</span>
                                </a>
                                <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next">
                                    <span class="carousel-control-next-icon" aria-hidden="true"></span>
                                    <span class="sr-only">Next</span>
                                </a>
                            </div>
                        </div>
                    </div>
                    <div class="col-6">
                        <span class="float-right pt-4">
                            <span class="float-right">Occupancy : @HotelBooking.OrderDetails.HotelRoomDTO.Occupancy adults </span><br />
                            <span class="float-right pt-1">Size : @HotelBooking.OrderDetails.HotelRoomDTO.SqFt sqft</span><br />
                            <h4 class="text-warning font-weight-bold pt-5">
                                <span style="border-bottom:1px solid #ff6a00">
                                    @HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount.ToString("#,#.00#;(#,#.00#)")
                                </span>
                            </h4>
                            <span class="float-right">Cost for @HotelBooking.OrderDetails.HotelRoomDTO.TotalDays nights</span>
                        </span>
                    </div>
                </div>
                <div class="row p-2">
                    <div class="col-12">
                        <p class="card-title text-warning" style="font-size:xx-large">@HotelBooking.OrderDetails.HotelRoomDTO.Name</p>
                        <p class="card-text" style="font-size:large">
                            @((MarkupString)@HotelBooking.OrderDetails.HotelRoomDTO.Details)
                        </p>
                    </div>

                </div>
            </div>
}
ب) نمایش فرم متصل به مدل کامپوننت
قسمت دوم UI کامپوننت جاری، نمایش فرم زیر است که اجزای مختلف آن به فیلد HotelBooking متصل شده‌اند:
@if (HotelBooking?.OrderDetails?.HotelRoomDTO?.HotelRoomImages == null)
{
    <div class="spinner"></div>
}
else
{
  // ...
             
            <div class="col-12 col-lg-5 p-4 2 mt-4 mt-md-0" style="border: 1px solid gray;">
                <EditForm Model="HotelBooking" class="container" OnValidSubmit="HandleCheckout">
                    <div class="row px-2 text-success border-bottom"><div class="col-7 py-1"><p style="font-size:x-large;margin:0px;">Enter Details</p></div></div>

                    <div class="form-group pt-2">
                        <label class="text-warning">Name</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Name" type="text" class="form-control" />
                    </div>
                    <div class="form-group pt-2">
                        <label class="text-warning">Phone</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Phone" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Email</label>
                        <InputText @bind-Value="HotelBooking.OrderDetails.Email" type="text" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check in Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckInDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">Check Out Date</label>
                        <InputDate @bind-Value="HotelBooking.OrderDetails.CheckOutDate" type="date" disabled class="form-control" />
                    </div>
                    <div class="form-group">
                        <label class="text-warning">No. of nights</label>
                        <select class="form-control" value="@NoOfNights" @onchange="HandleNoOfNightsChange">
                            @for (var i = 1; i <= 10; i++)
                            {
                                if (i == NoOfNights)
                                {
                                    <option value="@i" selected="selected">@i</option>
                                }
                                else
                                {
                                    <option value="@i">@i</option>
                                }
                            }
                        </select>
                    </div>
                    <div class="form-group">
                        <button type="submit" class="btn btn-success form-control">Checkout Now</button>
                    </div>
                </EditForm>
            </div>
        </div>
    </div>
}
در این فرم دو روال رویدادگردان زیر نیز مورد استفاده هستند:
@code {
    // ...

    private async Task HandleNoOfNightsChange(ChangeEventArgs e)
    {
        NoOfNights = Convert.ToInt32(e.Value.ToString());
        HotelBooking.OrderDetails.HotelRoomDTO = await HotelRoomService.GetHotelRoomDetailsAsync(
            Id.Value,
            HotelBooking.OrderDetails.CheckInDate,
            HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights));

        HotelBooking.OrderDetails.CheckOutDate = HotelBooking.OrderDetails.CheckInDate.AddDays(NoOfNights);
        HotelBooking.OrderDetails.HotelRoomDTO.TotalDays = NoOfNights;
        HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount =
                NoOfNights * HotelBooking.OrderDetails.HotelRoomDTO.RegularRate;
    }

    private async Task HandleCheckout()
    {
        if (!await HandleValidation())
        {
            return;
        }
    }

    private async Task<bool> HandleValidation()
    {
        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Name))
        {
            await JsRuntime.ToastrError("Name cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Phone))
        {
            await JsRuntime.ToastrError("Phone cannot be empty");
            return false;
        }

        if (string.IsNullOrEmpty(HotelBooking.OrderDetails.Email))
        {
            await JsRuntime.ToastrError("Email cannot be empty");
            return false;
        }
        return true;
    }
}
- کاربر اگر تعداد شب‌های اقامت را از طریق دارپ‌داون فرم تغییر داد، در روال رویدادگردان HandleNoOfNightsChange، محاسبات جدیدی را بر این اساس انجام می‌دهیم؛ چون هزینه و سایر مشخصات جزئیات اتاق نمایش داده شده، باید تغییر کنند.
- همچنین کدهای ابتدایی HandleCheckout را که برای ثبت نهایی اطلاعات فرم است، تهیه کرده‌ایم. البته در این قسمت این مورد را فقط محدود به اعتبارسنجی دستی و سفارشی که در متد HandleValidation مشاهده می‌کنید، کرده‌ایم. این روش دستی را نیز می‌توان برای تعریف منطق اعتبارسنجی یک فرم بکار برد و آن‌را توسط کدهای #C تکمیل کرد. البته باید درنظر داشت که data annotation validator توکار، هنوز از اعتبارسنجی خواص تو در تو، پشتیبانی نمی‌کند. به همین جهت است که در اینجا خودمان این اعتبارسنجی را به صورت دستی تعریف کرده‌ایم.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-29.zip