مطالب
آشنایی با Column Store Index در SQL Server 2012

 Column Store Index یکی از ویژگیهای جدید SQL Server 2012 می باشد، که کارایی Query های قایل اجرا روی دیتابیس‌های با حجم داده ای بسیار بالا را (که اصطلاحا به آنها Data Warehouse یا انبار داده گویند)، چندین برابر بهبود بخشیده است

 قبل از توضیح در مورد Column Store مختصری در مورد نحوه ذخیره سازی داده‌ها در SQL Server می پردازیم. می‌توان گفت در SQL Server دو روش ذخیره سازی وجود دارد،یکی بصورت ردیفی که اصطلاحا به آن  Row Storeیا  Row-Wise گویند، و دیگری بصورت ستونی که اصطلاحا به آن Column Store گویند

در روش ذخیره سازی Row Store، مقادیر ستونها در یک سطر بصورت متوالی ذخیره می‌شوند، در این روش ذخیره سازی از ساختار B-Tree یا Heap استفاده می‌شود.

یادآوری: در ساختار B-Tree، یک گره Root وجود دارد، و گره بعد از Root گره ای است که آدرس گره راست بعدی و آدرس گره چپ بعدی را در خود نگه می‌دارد.

شکل زیر نمای یک درخت B-Tree می‌باشد:

جهت کسب اطلاعات بیشتر درمورد ساختار B-Tree 

یادآوری: وقتی در یک جدول، ایندکسی از نوع Clustered ایجاد نماییم، SQL Server، در ابتدا یک کپی از جدول ایجاد و داده‌های جدول را از نو مرتب می‌نماید، و ساختار صفحه ریشه و دیگر صفحات را ایجاد می‌کند و سپس جدول اصلی را حذف می‌نماید. به جدولی که Clustered Index ندارد، اصطلاحا Heap گویند.

  برخلاف ذخیره سازی Row Store، در ذخیره سازی Column Store، داده‌ها بصورت ستونی ذخیره می‌شوند،در این روش داده ها، فشرده سازی می‌شوند و اینکار باعث می‌شود،در زمان درخواست یک Query، نیاز به Disk I/o به حداقل برسد، در نتیجه، زمان و سرعت پاسخگویی به پرس و جو‌ها بسیار افزایش می‌یابد.

شکل زیر نحوه ذخیره سازی داده ها،بصورت Row Store را نمایش می‌دهد:

  شکل بالا ذخیره سازی داده ها، در ساختار B-Tree یا Heap را نمایش می‌دهد، در شکل فوق یک جدول چهار ستونی با N سطر (Row) در نظر گرفته شده است.بطوریکه ستونهای هر Row بطور متوالی در یک صفحه (Page) یکسان ذخیره می‌شوند.

 شکل زیر نحوه ذخیره سازی داده ها،بصورت Column Store را نمایش می‌دهد:

  مطابق شکل،ستونهای مربوط به هر Row،همگی در یک صفحه (Page) یکسان ذخیره شده اند. به عنوان مثال ستون C1 که مربوط به سطر اول (Row1) می‌باشد، با ستون C1 که مربوط به سطر دوم (Row2) می‌باشد، در یک ستون و در یک صفحه (Page1) ذخیره شده اند، و الی آخر ...

   سئوال: یکبار دیگر به هردو شکل با دقت نگاهی بیاندازید، عمده تفاوت آنها در چیست؟

   جواب: درست حدس زدید، تفاوت بارز بین دو روش Column Store و Row Store در نحوه ذخیره سازی داده‌ها می‌باشد. بطور مثال، فرض کنید،در روش ذخیره سازی Row Store، به دنبال مقادیری از ستون C2 می‌باشید، SQL Server می‌بایست کل رکورد‌های جدول (منظور همه Row‌ها در همه Page ها)را Scan نماید، تا مقادیر مربوط به ستون C2 را بدست آورد.درحالیکه در روش ذخیره سازی Column Store، جهت یافتن مقادیر ستون C2، نیازی به Scan نمودن کل جدول نیست،بلکه SQL Server فقط به Scan نمودن ستون دوم (C2) یا Page2 بسنده می‌نماید.همین امر باعث افزایش چندین برابری، زمان پاسخگویی به هر Query می‌شود.

  سئوال: در روش ذخیره سازی Column Store، چگونه مصرف حافظه بهینه می‌شود؟

  جواب: واضح است،  که در روش SQL Server، Row Store مجبور است، برای بدست آوردن داده‌های مورد نظرتان،کل اطلاعات جدول را وارد حافظه نماید(اطلاعات اضافه ای که به هیچ وجه بدرد، نتیجه پرس و جوی شما نمی‌خورد)، و شروع به Scan داده‌های مد نظر شما می‌نماید.بطوریکه در روش SQL Server، Column Store، فقط ستون داده‌های مورد پرس و جو را در حافظه قرار می‌دهد.(در واقع فقط داده هایی را در حافظه قرار می‌دهد، که شما به آن نیاز دارید)،بنابراین،طبیعی است که در روش Column Store مقدار حافظه کمتری نسبت به روش Row Store در هنگام اجرای Query استفاده می‌شود. به عبارت دیگر می‌توان گفت که در روش Column Store به دلیل، به حداقل رساندن استفاده از Disk I/o سرعت و زمان پاسخگویی به پرس و جو‌ها چندین برابر می‌شود.

  برای درک بیشتر Row Store و Column Store مثالی می‌زنیم:

   فرض کنید،قصد بدست آوردن ستونهای C1 و C2 از جدول A را داریم، بنابراین خواهیم داشت:

Select C1, C2 from A

روش Row Store:

    در این روش همه صفحات دیسک (مربوط به جدول A) درون حافظه قرار داده می‌شود، یعنی علاوه بر ستونهای C1 و C2، اطلاعات مربوط به ستونهای C3 و C4 نیز درون حافظه قرار می‌گیرد،بطوریکه مقادیر ستونهای C3 و C4 به هیچ وجه مورد قبول ما نیست، و در خروجی پرس و جوی ما تاثیری ندارد، و فقط بی جهت حافظه اشغال می‌نماید.

روش Column Store:

  در این روش فقط صفحات مروبط به ستون C1 و C2 در حافظه قرار می‌گیرد.(منظور Page1 و Page2 می‌باشد) بنابراین فقط اطلاعات مورد نیاز در خروجی، در حافظه قرار می‌گیرد.

  •  از دیگر مزایای استفاده از روش Column Store، فشرده سازی داده می‌باشد،برای درک بیشتر توضیح می‌دهم:
      همانطور که در اوایل مطلبم به عرض رساندم، در روش Row Store ، داده‌ها در یک سطر و در یک Page ذخیره می‌شوند، بنابراین امکان وجود داده‌های تکراری در یک سطر به حداقل می‌رسد، چرا که، اگر فرض کنیم چهار ستون  به نام‌های ID،FirstName،LastName و City، داشته باشیم،در آن صورت بطور حتم،در یک سطر، داده تکراری وجود نخواهد داشت، اما ممکن است در تعداد سطرهای زیاد داده‌های تکراری مانند Firstname یا City و غیرو بوجود بیاید، این موضوع را بیان کردم، چون می‌خواستم عنوان کنم،بسیاری از الگوریتم‌های فشرده سازی از الگوی تکراری بودن داده، جهت فشرده سازی داده‌ها استفاده می‌کنند، به همین جهت فشرده سازی در روش Row Store به حداقل می‌رسد و فضای اشغال شده در حافظه دراین روش بسیار زیاد خواهد بود. اما در روش Column Store ، امکان تکراری بودن مقادیر یک ستون بسیار زیاد  است، بطور مثال ممکن است تعداد افرادی را که نام شهر  آنها "تهران" باشد مثلا 20 بار تکرار شده باشد، و چون در روش Column Store، ستون‌ها در یک Page ذخیره می‌شوند، بنابراین امکان استفاده از الگوریتمهای فشرده سازی در این روش بسیار بالا می‌باشد، در نتیجه مقدار فضایی را که در حافظه یا دیسک سخت توسط این روش اشغال می‌شود، بسیار کمتر از روش Row Store است.

چه موقع می‌توانیم از Column Store استفاده نماییم:

   در تعریف Column Store گفته بودم، روش فوق، جهت بهبود بخشیدن به زمان و سرعت پاسخگویی به Query‌های اجرا شده روی دیتابیس‌های با حجم داده ای بسیار بالا(Data Warehouse ) می‌باشد، به بیان ساده‌تر Column Store را روی دیتابیس‌های offline یا دیتابیسهایی که صرفا جهت گزارش گیری مورد استفاده قرار می‌گیرند، تنظیم می‌نمایند.در واقع با تنظیم Column Store Index روی Database‌های بزرگ مانند Database‌های بانک‌ها که حجم داده ای میلیونی در جداول آنها وجود دارد، سرعت پاسخگویی Query ها، چندین برابر افزایش می‌یابد.

  •      در یک جدول می‌توانید، هم Column Store Index داشته باشید و هم یک Row Store Index (منظور یک  Clustered Index می باشد)
  • Syntax برای ایجاد  Column Store Index به شرح ذیل می‌باشد:
CREATE [ NONCLUSTERED ] COLUMNSTORE INDEX index_name 
    ON <object> ( column  [ ,...n ] )
    [ WITH ( <column_index_option> [ ,...n ] ) ]
    [ ON {
           { partition_scheme_name ( column_name ) } 
           | filegroup_name 
           | "default" 
         }
    ]
[ ; ]

<object> ::=
{
    [database_name. [schema_name ] . | schema_name . ]
     table_name
{

<column_index_option> ::=
{
      DROP_EXISTING = { ON | OFF }
    | MAXDOP = max_degree_of_parallelism
 }
  • یک Column Store Index می‌بایست از نوع NONCLUSTERED باشد.
مثال از یک Column Store Index :
CREATE NONCLUSTERED COLUMNSTORE INDEX [IX_MyFirstName_ColumnStore]
ON [Test]
(Firstname)
در قطعه کد بالا، یک Column Store Index به نام Ix_MyFirstname_ColumnStore روی فیلد Firstname از جدول Test ایجاد شده است.
محدودیت‌های استفاده از Column Store Index به اختصار به شرح ذیل می‌باشد:
  • زمانی که در یک جدول، یک Column Store Index ایجاد نماییم، جدول ما  در حالت Read-only قرار می‌گیرد، بطوریکه از آن پس  اختیار Delete،Update و Insert روی جدول فوق را نخواهیم داشت. برای اینکه بتوانید عملیات Insert، Update یا Delete را انجام دهید، میبایست Column Store Index جدول مربوطه را Disable نمایید، و برای فعال نمودن Column Store Index، می‌بایست آن را Rebuild نمایید، با کلیک راست روی ایندکس ایجاد شده در SQL Server2012 موارد Disable و Rebuild قابل مشاهده می‌باشد.

یا بوسیله Script‌های زیر می‌توانید، عملیات Disable یا Rebuild را روی Column Store Index انجام دهید:
ALTER INDEX [IX_MyFirstName_ColumnStore] ON [Test] DISABLE

ALTER INDEX [IX_MyFirstName_ColumnStore] ON [Test] Rebuild
  • بیشتر از یک Column Store Index نمی‌توانید روی یک جدول ایجاد نمایید.
  • در صورتی که تمایل داشته باشید بوسیله Alter ، نوع فیلدی (Type)، را که Column Store Index روی آنها اعمال گردیده است، تغییر دهید، در ابتدا می‌بایست Column Store Index، خود را Drop یا حذف نمایید، سپس عملیات Alter را اعمال کنید، در غیر اینصورت با خطای SQL Server مواجه می‌شوید.
  • یک Column Store Index می‌تواند روی 1024 ستون در یک جدول اعمال گردد.
  • یک Column Store Index  نمی توانند، Unique باشد و نمی‌توان از آن به عنوان Primary Key یا Foreign Key استفاده نمود.
یاد آوری: با توجه به مزایای استفاده از Column Store Index، باید بگویم که در حجم‌های داده ای کم استفاده از Row Store Index بهتر می‌باشد. پیشنهاد مایکروسافت برای استفاده از Column Store Index برای دیتابیس‌های با حجم داده ای بسیار بالا می‌باشد.
موفق باشید
منابع:

مطالب
گروه بندی اطلاعات و گزارشات Master-Details در PdfReport
اگر به بانک اطلاعاتی مثال‌های همراه سورس‌های PdfReport در مسیر Bin\Data\blogs.sqlite مراجعه کنید، دو جدول والدین و فرزندان هم در آن وجود دارند:



بر این اساس قصد داریم رابطه یک به چند فوق را گروه بندی شده نمایش دهیم:


(البته این اعداد و اطلاعات، به صورت اتفاقی تولید شده‌اند و الزامی ندارد که والد متولد 2002 هنوز والد شده باشد؛ یا اینکه فرزندی متولد 2003 داشته باشد!)

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

سورس کامل این مثال را در ادامه مشاهده می‌کنید:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.MasterDetails
{
    public class MasterDetailsPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                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(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\arial.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesHeader(header =>
            {
                header.CustomHeader(new MasterDetailsHeaders { PdfRptFont = header.PdfFont });
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .MainTableTemplate(t => t.BasicTemplate(BasicTemplate.SilverTemplate))
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = false,
                    SpacingBeforeAllGroupsSummary = 5f,
                    NewGroupAvailableSpacingThreshold = 170
                });
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.GenericDataReader(
                   providerName: "System.Data.SQLite",
                   connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                   sql: @"select 
                            tblParents.BirthDate as ParentBirthDate,
                            tblParents.Name as ParentName,
                            tblParents.LastName as ParentLastName,
                            tblKids.Name as KidName,
                            tblKids.BirthDate as KidBirthDate
                            from tblParents
                                left outer join tblKids
                                     on tblKids.ParentId = tblParents.Id
                            order by 
                                tblParents.Name,
                                tblParents.LastName,
                                tblKids.Name"
               );
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Left);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentBirthDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("ParentBirthDate");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        var date1 = (DateTime)val1;
                        var date2 = (DateTime)val2;
                        return date1.Year == date2.Year && date1.Month == date2.Month && date1.Day == date2.Day;
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("ParentName");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentLastName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("ParentLastName");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("KidName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("Child Name");
                    column.IsVisible(true);
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("KidBirthDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(5);
                    column.Width(2);
                    column.HeaderCell("BirthDate");
                    column.IsVisible(true);
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(e => e.ToExcel())
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptMasterDetailsSample.pdf"));
        }
    }
}
به همراه سر ستون‌های مجزای هر گروه و صفحه:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.ColumnsItemsTemplates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.MasterDetails
{
    public class MasterDetailsHeaders : IPageHeader
    {
        public IPdfFont PdfRptFont { set; get; }

        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
            var parentName = newGroupInfo.GetSafeStringValueOf("ParentName");
            var parentLastName = newGroupInfo.GetSafeStringValueOf("ParentLastName");
            var parentBirthDate = newGroupInfo.GetSafeStringValueOf("ParentBirthDate");

            var table = new PdfPTable(relativeWidths: new[] { 1f, 5f }) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Last Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentLastName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.Value = "Birth Date:";
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
               },
               (cellData, cellProperties) =>
               {
                   cellData.Value = parentBirthDate;
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
               });
            return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 5f);
        }

        public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            var table = new PdfPTable(numColumns: 1) { WidthPercentage = 100 };
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.CellTemplate = new ImageFilePathField();
                   cellData.Value = AppPath.ApplicationPath + "\\Images\\01.png";
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
               });
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.Value = "Family rpt";
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
               });
            return table.AddBorderToTable();
        }
    }
}
توضیحات:
- منبع داده مورد استفاده در اینجا از نوع GenericDataReader است؛ جهت خواندن رکوردهای بانک اطلاعاتی SQLite ذکر شده در ابتدای بحث. (دو مثال دیگر هم به پوشه مثال‌های سورس‌های PdfReport اضافه شده‌اند به نام‌های Grouping و WrapGroupsInColumns که به همین موضوع گروه بندی می‌پردازند؛ البته با استفاده از StronglyTypedList‌ها. ولی درکل مفاهیم و اصول آن‌ها یکی است.)
select 
          tblParents.BirthDate as ParentBirthDate,
          tblParents.Name as ParentName,
          tblParents.LastName as ParentLastName,
          tblKids.Name as KidName,
          tblKids.BirthDate as KidBirthDate
          from tblParents
                  left outer join tblKids
                        on tblKids.ParentId = tblParents.Id
                            order by 
                                tblParents.Name,
                                tblParents.LastName,
                                tblKids.Name
در کوئری فوق (و کلا گروه بندی اطلاعات) دو نکته حائز اهمیت است:
الف) چون قرار است اطلاعات بر اساس مشخصات والدین و فرزندان آن‌ها گروه بندی شود، نیاز است حتما order by  و مرتب سازی رکوردها قید گردد.
ب) در PdfReport نمی‌توانید در خواص معرفی شده جهت تعریف ستون‌ها، از نام‌های تکراری استفاده کنید. برای رفع این مشکل استفاده از Alias پیشنهاد می‌شود؛ مانند:
tblParents.Name as ParentName,
tblKids.Name as KidName,
- مشخص سازی خاصیت و ستونی که قرار است در گروه بندی شرکت کند بسیار ساده است:
column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
در اینجا به کمک متد Group، قابلیت گروه بندی بر روی این ستون فعال شده و سپس باید فرمولی را جهت مشخص سازی حد و مرز گروه مشخص کنیم. برای مثال در اینجا اگر مقادیر ردیف جاری (val2) و ردیف قبلی (val1) یکسان نبودند، یعنی گروه خاتمه یافته و گروه جدیدی شروع می‌شود (به همین جهت عنوان شد که مرتب سازی اطلاعات ضروری است).

- تنظیم دیگری را که در اینجا می‌توان ذکر کرد، مورد ذیل است:
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = false,
                    SpacingBeforeAllGroupsSummary = 5f,
                    NewGroupAvailableSpacingThreshold = 170
                });
به این ترتیب می‌توان مشخص کرد که آیا باید ستون‌های دخیل در گروه بندی، در گزارش نمایش داده شوند یا خیر (GroupType.HideGroupingColumns)، آیا سر ستون هر جدول، به ازای هر گروه باید تکرار شود؟ (RepeatHeaderRowPerGroup)، آیا در هر صفحه یک گروه نمایش داده شود (ShowOneGroupPerPage) یا اینکه گروه‌ها به صورت متوالی در صفحات درج شوند. توسط SpacingBeforeAllGroupsSummary، فاصله جمع نهایی تمام گروه‌ها از آخرین گروه نمایش داده شده مشخص می‌شود. به کمک NewGroupAvailableSpacingThreshold مشخص می‌کنیم که در چه فاصله‌ای از انتهای صفحه، گروه جدیدی نباید درج شود و این گروه باید به صفحه بعدی منتقل شده و از آنجا شروع شود.

- اگر به تصویر ابتدای مطلب دقت کرده باشید، علاوه بر هدر صفحه، هر گروه نیز یک هدر مجزا دارد. برای طراحی آن باید اینترفیس IPageHeader را پیاده سازی کرد که نمونه‌ای از آن‌را در کلاس MasterDetailsHeaders فوق مشاهده می‌کنید.
        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
            var parentName = newGroupInfo.GetSafeStringValueOf("ParentName");
            var parentLastName = newGroupInfo.GetSafeStringValueOf("ParentLastName");
            var parentBirthDate = newGroupInfo.GetSafeStringValueOf("ParentBirthDate");

            var table = new PdfPTable(relativeWidths: new[] { 1f, 5f }) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
ساختار آن هم بسیار ساده است. توسط  newGroupInfo می‌توان به اطلاعات گروه جدید، دسترسی یافت. برای مثال در اینجا اطلاعات والد گروه جدید در حال تهیه، دریافت شده و سپس در ردیف‌های یک جدول دو ستونه درج می‌شود. در ستون اول آن یک برچسب و در ستون دوم، مقدار دریافتی نمایش داده شده است و همینطور الی آخر برای سایر ردیف‌ها.
مطالب
SignalR
چند وقتی هست که در کنار بدنه اصلی دات‌نت فریم‌ورک چندین کتابخونه به صورت متن‌باز در حال توسعه هستند. این مورد در ASP.NET بیشتر فعاله و مثلا دو کتابخونه SignalR و WebApi توسط خود مایکروسافت توسعه داده میشه.
SignalR همونطور که در سایت بسیار خلاصه و مفید یک صفحه‌ای! خودش توضیح داده شده (^) یک کتابخونه برای توسعه برنامه‌های وب «زمان واقعی»! (real-time web) است:
Async library for .NET to help build real-time, multi-user interactive web applications.
برنامه‌های زمان واقعی به صورت خلاصه و ساده به‌صورت زیر تعریف میشن (^):
The real-time web is a set of technologies and practices that enable users to receive information as soon as it is published by its authors, rather than requiring that they or their software check a source periodically for updates.
یعنی کاربر سیستم ما بدون نیاز به ارسال درخواستی صریح! برای دریافت آخرین اطلاعات به روز شده در سرور، در برنامه کلاینتش از این تغییرات آگاه بشه. مثلا برنامه‌هایی که برای نمایش نمودارهای آماری داده‌ها استفاده میشه (بورس، قیمت ارز و طلا و ...) و یا مهمترین مثالش میتونه برنامه «چت» باشه. متاسفانه پروتوکل HTTP مورد استفاده در وب محدودیت‌هایی برای پیاده‌سازی این گونه برنامه‌ها داره. روش‌های گوناگونی برای پیاده‌سازی برنامه‌های زمان واقعی در وب وجود داره که کتابخونه SignalR فعلا از موارد زیر استفاده میکنه:
  1. تکنولوژی جدید WebSocket (^) که خوشبختانه پشتیبانی کاملی از اون در دات نت 4.5 (چهار نقطه پنج! نه چهار و نیم!) وجود داره. اما تمام مرورگرها و تمام وب سرورها از این تکنولوژی پشتیبانی نمیکنند و تنها برخی نسخه‌های جدید قابلیت استفاده از آخرین ورژن WebSocket رو دارند که میشه به کروم 16 به بالا و فایرفاکس 11 به بالا و اینترنت اکسپلورر 10 اشاره کرد (برای استفاده از این تکنولوژی در ویندوز نیاز به IIS 8.0 است که متاسفانه فقط در ویندوز 8.0 موجوده):
    Chrome 16, Firefox 11 and Internet Explorer 10 are currently the only browsers supporting the latest specification (RFC 6455).
  2.  یه روش دیگه Server-sent Events نام داره که داده‌های جدید رو به فرم رویدادهای DOM به سمت کلاینت میفرسته(^).
  3. روش دیگه‎‌ای که موجوده به Forever Frame معروفه که در این روش یک iframe مخفی درون کد html مسئول تبادل داده‌هاست. این iframe مخفی به‌صورت یک بلاک Chunked (^) به سمت کلاینت فرستاده میشه. این iframe که مسئول رندر داده‌های جدید در سمت کلاینت هست ارتباط خودش رو با سرور تا ابد! (برای همین بهش forever میگن) حفظ میکنه. هر وقت رویدادی سمت سرور رخ میده با استفاده از این روش داده‌ها به‌صورت تگ‌های script به این فریم مخفی فرستاده می‌شوند و چون مرورگرها محتوای html رو به صورت افزایشی (incrementally) رندر میکنن بنابراین این اسکریپتها به‌ترتیب زمان دریافت اجرا می‌شوند. (البته ظاهرا عبارت forever frame در صنعت عکاسی! معروف‌تره بنابراین در جستجو در زمینه این روش ممکنه کمی مشکل داشته باشین) (^).
  4. روش آخر که در کتابخونه SignalR ازش استفاده میشه long-polling نام داره. در روش polling معمولی پس از ارسال درخواست توسط کلاینت، سرور بلافاصله نتیجه حاصله رو به سمت کلاینت میفرسته و ارتباط قطع میشه. بنابراین برای داده‌های جدید درخواست جدیدی باید به سمت سرور فرستاده بشه که تکرار این روش باعث افزایش شدید بار بر روی سرور و کاهش کارآمدی اون می‌شه. اما در روش long-polling پس از برقراری ارتباط کلاینت با سرور این ارتباط تا مدت زمان معینی (که توسط یه مقدار تایم اوت مشخص میشه و مقدار پیش‌فرضش 2 دقیقه است) برقرار میمونه. بنابراین کلاینت میتونه بدون ایجاد مشکلی در کارایی، داده‌های جدید رو از سرور دریافت کنه. به این روش در برنامه‌نویسی وب اصطلاحا برنامه‌نویسی کامت (Comet Programming) میگن (^ ^).
(البته روش‌های دیگری هم برای پیاده‌سازی برنامه‌های زمان اجرا وجود داره مثل کتابخونه node.js که جستجوی بیشتر به خوانندگان واگذار میشه)
SignalR برای برقراری ارتباط ابتدا بررسی میکنه که آیا هر دو سمت سرور و کلاینت قابلیت پشتیبانی از WebSocket رو دارند. در غیراینصورت سراغ روش Server-sent Events میره. اگر باز هم موفق نشد سعی به برقراری ارتباط با روش forever frame میکنه و اگر باز هم موفق نشد در آخر سراغ long-polling میره.
با استفاده از SignalR شما میتونین از سرور، متدهایی رو در سمت کلاینت فراخونی کنین. یعنی درواقع با استفاده از کدهای سی شارپ میشه متدهای جاوااسکریپت سمت کلاینت رو صدا زد!
بطور خلاصه در این کتابخونه دو کلاس پایه وجود داره:
  1. کلاس سطح پایین PersistentConnection
  2. کلاس سطح بالای Hub
علت این نامگذاری به این دلیله که کلاس سطح پایین پیاده‌سازی پیچیده‌تر و تنظیمات بیشتری نیاز داره اما امکانات بیشتری هم در اختیار برنامه‌نویس قرار می‌ده.
خوب پس از این مقدمه نسبتا طولانی برای دیدن یک مثال ساده میتونین با استفاده از نوگت (Nuget) مثال زیر رو نصب و اجرا کنین (اگه تا حالا از نوگت استفاده نکردین قویا پیشنهاد میکنم که کار رو با دریافتش از اینجا آغاز کنین) :
PM> Install-Package SignalR.Sample
پس از کامل شدن نصب این مثال اون رو اجرا کنین. این یک مثال فرضی ساده از برنامه نمایش ارزش آنلاین سهام برخی شرکتهاست. میتونین این برنامه رو همزمان در چند مرورگر اجرا کنین و نتیجه رو مشاهده کنین.
حالا میریم سراغ یک مثال ساده. میخوایم یک برنامه چت ساده بنویسیم. ابتدا یک برنامه وب اپلیکیشن خالی رو ایجاد کرده و با استفاده از دستور زیر در خط فرمان نوگت، کتابخونه SignalR رو نصب کنین:
PM> Install-Package SignalR
پس از کامل شدن نصب این کتابخونه، ریفرنس‌های زیر به برنامه اضافه میشن:
Microsoft.Web.Infrastructure
Newtonsoft.Json
SignalR
SignalR.Hosting.AspNet
SignalR.Hosting.Common
برای کسب اطلاعات مختصر و مفید از تمام اجزای این کتابخونه به اینجا مراجعه کنین.
همچنین اسکریپت‌های زیر به پوشه Scripts اضافه میشن (این نسخه‌ها مربوط به زمان نگارش این مطلب است):
jquery-1.6.4.js
jquery.signalR-0.5.1.js
بعد یک کلاس با نام SimpleChat به برنامه اضافه و محتوای زیر رو در اون وارد کنین:
using SignalR.Hubs;
namespace SimpleChatWithSignalR
{
  public class SimpleChat : Hub
  {
    public void SendMessage(string message)
    {
      Clients.reciveMessage(message);
    }
  }
} 
دقت کنین که این کلاس از کلاس Hub مشتق شده و همچنین خاصیت Clients از نوع dynamic است. (در مورد جزئیات این کتابخونه در قسمت‌های بعدی توضیحات مفصل‌تری داده میشه)
سپس یک فرم به برنامه اضافه کرده و محتوای زیر رو در اون اضافه کنین:
<input type="text" id="msg" />
<input type="button" value="Send" id="send" /><br />
<textarea id='messages' readonly="true" style="height: 200px; width: 200px;"></textarea>
<script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="Scripts/jquery.signalR-0.5.1.min.js" type="text/javascript"></script>
<script src="signalr/hubs" type="text/javascript"></script>
<script type="text/javascript">
  var chat = $.connection.simpleChat;
  chat.reciveMessage = function (msg) {
   $('#messages').val($('#messages').val() + "-" + msg + "\r\n"); 
  };
  $.connection.hub.start();
  $('#send').click(function () {
    chat.sendMessage($('#msg').val());
  });
</script>
همونطور که میبینین برنامه چت ما آماده شد! حالا برنامه رو اجرا کنین و با استفاده از دو مرورگر مختلف نتیجه رو مشاهده کنین.
نکته کلیدی کار SignalR در خط زیر نهفته است:
<script src="signalr/hubs" type="text/javascript"></script>
اگر محتوای آدرس فوق رو دریافت کنین می‌بینین که موتور این کتابخانه تمامی متدهای موردنیاز در سمت کلاینت رو با استفاده از کدهای جاوااسکریپت تولید کرده. البته در این کد تولیدی از نامگذاری camel Casing استفاده میشه، بنابراین متد SendMessage در سمت سرور به‌صورت sendMessage در سمت کلاینت در دسترسه.
امیدوارم تا اینجا تونسته باشم علاقه شما به استفاده از این کتابخونه رو جلب کرده باشم. در قسمت‌های بعد موارد پیشرفته‌تر این کتابخونه معرفی میشه.
اگه علاقه‌مند باشین میتونین از این ویکی اطلاعات بیشتری بدست بیارین.


به روز رسانی
در دوره‌ای به نام SignalR در سایت، به روز شده‌ای این مباحث را می‌توانید مطالعه کنید.
مطالب
EF Code First #5

در قسمت قبل خاصیت AutomaticMigrationsEnabled را در کلاس Configuration به true تنظیم کردیم. به این ترتیب، عملیات ساده شده، اما یک سری از قابلیت‌های ردیابی تغییرات را از دست خواهیم داد و این عملیات،‌ صرفا یک عملیات رو به جلو خواهد بود.
اگر AutomaticMigrationsEnabled را مجددا به false تنظیم کنیم و هربار به کمک دستوارت Add-Migration و Update-Database تغییرات مدل‌ها را به بانک اطلاعاتی اعمال نمائیم، علاوه بر تشکیل تاریخچه این تغییرات در برنامه، امکان بازگشت به عقب و لغو تغییرات صورت گرفته نیز مهیا می‌گردد.

هدف قرار دادن مرحله‌ای خاص یا لغو آن

به همان پروژه قسمت قبل مراجعه نمائید. در کلاس Configuration آن، خاصیت AutomaticMigrationsEnabled را به false تنظیم کنید. سپس یک خاصیت جدید را به کلاس Project اضافه نموده و برنامه را اجرا نمائید. بلافاصله خطای زیر را دریافت خواهیم کرد:

Unable to update database to match the current model because there are pending changes and 
automatic migration is disabled. Either write the pending model changes to a code-based migration
or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true
to enable automatic migration.

EF تشخیص داده است که کلاس مدل برنامه، با بانک اطلاعاتی تطابق ندارد و همچنین ویژگی مهاجرت خودکار نیز فعال نیست. بنابراین اعمال code-based migration را توصیه کرده است.
برای این منظور به کنسول پاورشل NuGet مراجعه نمائید (منوی Tools در ویژوال استودیو، گزینه‌ Library package manager آن و سپس انتخاب گزینه package manager console). در ادامه فرمان add-m را نوشته و دکمه tab را فشار دهید. یک منوی Auto Complete ظاهر خواهد شد که از آن‌ می‌توان فرمان add-migration را انتخاب نمود. در اینجا یک نام را هم نیاز است وارد کرد؛ برای مثال:

Add-Migration AddSomeProp2ToProject

به این ترتیب کلاس زیر را به صورت خودکار تولید خواهد کرد:

namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;

public partial class AddSomeProp2ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp", c => c.String());
AddColumn("Projects", "SomeProp2", c => c.String());
}

public override void Down()
{
DropColumn("Projects", "SomeProp2");
DropColumn("Projects", "SomeProp");
}
}
}

مدل‌های برنامه را با بانک اطلاعاتی تطابق داده و دریافته است که هنوز دو خاصیت در اینجا به بانک اطلاعاتی اضافه نشده‌اند.
از متد Up برای اعمال تغییرات و از متد Down برای بازگشت به قبل استفاده می‌گردد. نام فایل این کلاس هم طبق معمول چیزی است شبیه به timeStamp_AddSomeProp2ToProject.cs .

در ادامه نیاز است این تغییرات به بانک اطلاعاتی اعمال شوند. به همین منظور دستور زیر را در کنسول پاورشل وارد نمائید:

Update-Database -Verbose

پارامتر Verbose آن سبب خواهد شد تا جزئیات عملیات به صورت مفصل گزارش داده شود که شامل دستورات ALTER TABLE نیز هست:

Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201205061835024_AddSomeProp2ToProject].
Applying explicit migration: 201205061835024_AddSomeProp2ToProject.
ALTER TABLE [Projects] ADD [SomeProp] [nvarchar](max)
ALTER TABLE [Projects] ADD [SomeProp2] [nvarchar](max)
[Inserting migration history record]

اکنون مجددا یک خاصیت دیگر را مثلا به نام public string SomeProp3، به کلاس Project اضافه نمائید.
سپس همین روال باید مجددا تکرار شود. دستورات زیر را در کنسول پاورشل NuGet اجرا نمائید:

Add-Migration AddSomeProp3ToProject
Update-Database -Verbose

اینبار نیز یک کلاس جدید به نام AddSomeProp3ToProject به پروژه اضافه خواهد شد و سپس بر اساس آن، امکان به روز رسانی بانک اطلاعاتی میسر می‌گردد.

در ادامه برای مثال به این نتیجه رسیده‌ایم که نیازی به خاصیت public string SomeProp3 اضافه شده، نبوده است. روش متداول، باز هم مانند سابق است. ابتدا خاصیت را از کلاس Project حذف خواهیم کرد و سپس دو دستور Add-Migration و Update-Database را اجرا خواهیم نمود.
اما با توجه به اینکه مهاجرت خودکار را غیرفعال کرده‌ایم و هربار با فراخوانی دستور Add-Migration یک کلاس جدید، با متدهای Up و Down به پروژه، جهت نگهداری سوابق عملیات اضافه می‌شوند، می‌توان دستور Update-Database را جهت فراخوانی متد Down صرفا یک مرحله موجود نیز فراخوانی نمود.

نکته:
اگر علاقمند باشید که راهنمای مفصل پارامترهای دستور Update-Database را مشاهده کنید، تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائید:

get-help update-database -detailed

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

در ادامه برای اینکه دستور Update-Database تنها یک مرحله مشخص را که سابقه آن در برنامه موجود است، هدف قرار دهد، باید از پارامتر TargetMigration به همراه نام کلاس مرتبط استفاده کرد:

Update-Database -TargetMigration:"AddSomeProp2ToProject" -Verbose

اگر دقت کرده باشید در اینجا AddSomeProp2ToProject بجای AddSomeProp3ToProject بکارگرفته شده است. اگر یک مرحله قبل را هدف قرار دهیم، متد Down را اجرا خواهد کرد:

Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Reverting migrations: [201205061845485_AddSomeProp3ToProject].
Reverting explicit migration: 201205061845485_AddSomeProp3ToProject.
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'Projects')
AND col_name(parent_object_id, parent_column_id) = 'SomeProp3';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [Projects] DROP CONSTRAINT ' + @var0)
ALTER TABLE [Projects] DROP COLUMN [SomeProp3]
[Deleting migration history record]

همانطور که ملاحظه می‌کنید در اینجا عملیات حذف ستون SomeProp3 انجام شده است. البته این خاصیت به صورت خودکار از کدهای برنامه (کلاس Project در این مثال) حذف نمی‌شود و فرض بر این است که پیشتر اینکار را انجام داده‌اید.


سفارشی سازی کلاس‌های مهاجرت

تمام کلاس‌های خودکار مهاجرت تولید شده توسط پاورشل، از کلاس DbMigration ارث بری می‌کنند. در این کلاس امکانات قابل توجهی مانند AddColumn، AddForeignKey، AddPrimaryKey، AlterColumn، CreateIndex و امثال آن وجود دارند که در تمام کلاس‌های مشتق شده از آن، قابل استفاده هستند. حتی متد Sql نیز در آن پیش بینی شده است که در صورت نیاز به اجرای دستوارت خام SQL، می‌توان از آن استفاده کرد.
برای مثال فرض کنید مجددا همان خاصیت public string SomeProp3 را به کلاس Project اضافه کرد‌ه‌ایم. اما اینبار نیاز است حین تشکیل این فیلد در بانک اطلاعاتی، یک مقدار پیش فرض نیز برای آن درنظر گرفته شود که در صورت نال بودن مقدار خاصیت آن در برنامه، به صورت خودکار توسط بانک اطلاعاتی مقدار دهی گردد:

namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;

public partial class AddSomeProp3ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp3", c => c.String(defaultValue: "some data"));
Sql("Update Projects set SomeProp3=N'some data'");
}

public override void Down()
{
DropColumn("Projects", "SomeProp3");
}
}
}

متد String در اینجا چنین امضایی دارد:

public ColumnModel String(bool? nullable = null, int? maxLength = null, bool? fixedLength = null, 
bool? isMaxLength = null, bool? unicode = null, string defaultValue = null, string defaultValueSql = null,
string name = null, string storeType = null)

که برای نمونه در اینجا پارامتر defaultValue آن‌را در کلاس AddSomeProp3ToProject مقدار دهی کرده‌ایم.
برای اعمال این تغییرات تنها کافی است دستور Update-Database -Verbose اجرا گردد. اینبار خروجی SQL اجرا شده آن به نحو زیر است که شامل مقدار پیش فرض نیز شده است:

ALTER TABLE [Projects] ADD [SomeProp3] [nvarchar](max) DEFAULT 'some data'

تعیین مقدار پیش فرض، زمانیکه یک فیلد not null تعریف شده‌است نیز می‌تواند مفید باشد. همچنین در اینجا امکان اجرای دستورات مستقیم SQL نیز وجود دارد که نمونه‌ای از آن‌را در متد Up فوق مشاهده می‌کنید.


افزودن رکوردهای پیش فرض در حین به روز رسانی بانک اطلاعاتی

در قسمت‌های قبل با متد Seed که به همراه آغاز کننده‌های بانک اطلاعاتی EF ارائه شده‌اند، جهت افزودن رکوردهای اولیه و پیش فرض به بانک اطلاعاتی آشنا شدید. در اینجا نیز با تحریف متد Seed در کلاس Configuration،‌ چنین امری میسر است:

namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity.Migrations;

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = false;
this.AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(EF_Sample02.Sample2Context context)
{
context.Users.AddOrUpdate(
a => a.Name,
new Models.User { Name = "Vahid", AddDate = DateTime.Now },
new Models.User { Name = "Test", AddDate = DateTime.Now });
}
}
}

متد AddOrUpdate در EF 4.3 اضافه شده است. این متد ابتدا بررسی می‌کند که آیا رکورد مورد نظر در بانک اطلاعاتی وجود دارد یا خیر. اگر خیر، آن‌را اضافه خواهد کرد در غیراینصورت، نمونه موجود را به روز رسانی می‌کند. اولین پارامتر آن، identifierExpression نام دارد. توسط آن مشخص می‌شود که بر اساس چه خاصیتی باید در مورد update یا add تصمیم‌گیری شود. دراینجا اگر نیاز به ذکر بیش از یک خاصیت وجود داشت، از anonymously type object می‌توان کمک گرفت new { p.Name, p.LastName } .


تولید اسکریپت به روز رسانی بانک اطلاعاتی

بهترین کار و امن‌ترین روش حین انجام این نوع به روز رسانی‌ها، تهیه اسکریپت SQL فرامینی است که باید بر روی بانک اطلاعاتی اجرا شوند. سپس می‌توان این دستورات و اسکریپت نهایی را دستی هم اجرا کرد (که روش متداول‌تری است در محیط کاری).
برای اینکار تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائیم:
Update-Database -Verbose -Script

پس از اجرای این دستور، یک فایل اسکریپت با پسوند sql تولید شده و بلافاصله در ویژوال استودیو جهت مرور نیز گشوده خواهد شد. برای نمونه محتوای آن برای افزودن خاصیت جدید SomeProp5 به صورت زیر است:

ALTER TABLE [Projects] ADD [SomeProp5] [nvarchar](max)
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES
('201205060852004_AutomaticMigration', '2012-05-06T08:52:00.937Z', 0x1F8B0800000............ '4.3.1')

همانطور که ملاحظه می‌کنید، در یک مرحله، جدول پروژه‌ها را به روز خواهد کرد و در مرحله بعد، سابقه آن‌را در جدول __MigrationHistory ثبت می‌کند.

یک نکته:
اگر دستور فوق را بر روی برنامه‌ای که با بانک اطلاعاتی هماهنگ است اجرا کنیم، خروجی را مشاهده نخواهیم کرد. برای این منظور می‌توان مرحله خاصی را توسط پارامتر SourceMigration هدف گیری کرد:

Update-Database -Verbose -Script -SourceMigration:"stepName"




استفاده از DB Migrations در عمل

البته این یک روش پیشنهادی و امن است:
الف) در ابتدای اجرا برنامه، پارامتر ورودی متد System.Data.Entity.Database.SetInitializer را به نال تنظیم کنید تا برنامه تغییری را بر روی بانک اطلاعاتی اعمال نکند.
ب) توسط دستور enable-migrations،‌ فایل‌های اولیه DB Migration را ایجاد کنید. پیش فرض‌های آن را نیز تغییر ندهید.
ج) هر بار که کلاس‌های مدل‌ برنامه تغییر کردند و پس از آن نیاز به به روز رسانی ساختار بانک اطلاعاتی وجود داشت دو دستور زیر را اجرا کنید:
Add-Migration AddSomePropToProject
Update-Database -Verbose -Script

به این ترتیب سابقه تغییرات در برنامه نگهداری شده و همچنین بدون اجرای دستورات بر روی بانک اطلاعاتی، اسکریپت نهایی اعمال تغییرات تولید می‌گردد.
د) اسکریپت تولید شده را بررسی کرده و پس از تائید و افزودن به سورس کنترل، به صورت دستی بر روی بانک اطلاعاتی اجرا کنید (مثلا توسط management studio).


مطالب
استفاده از گرافیک برداری در iTextSharp


در مورد «ترسیم اشکال گرافیکی با iTextSharp» مطلب مفصلی را در اینجا می‌توانید مطالعه کنید؛ که قصد تکرار مجدد آن‌را ندارم. فقط این روش‌ها یک مشکل مهم دارند : «کار من ترسیم این نوع اشکال گرافیکی نیست!». مثلا من الان نیاز دارم در گزارشی، بجای ستون Boolean آن در مواردی که مقدار ردیف true هست، مثلا یک «چک مارک» را بجای true/false یا بله/خیر نمایش دهم. می‌شود اینکار را با یک تصویر معمولی هم انجام داد. فقط حجم فایل حاصل، بیش از اندازه بالا می‌رود و همچنین نتیجه استفاده از یک bitmap، به زیبایی بکارگیری گرافیک برداری با قابلیت تغییر ابعاد بدون نگرانی در مورد از دست دادن کیفیت آن، نیست.

خوشبختانه هستند سایت‌هایی که این نوع تصاویر برداری را به رایگان ارائه دهند؛ برای مثال: سایت Openclipart، تعداد قابل توجهی فایل با فرمت SVG دارد. فایل‌های SVG را مستقیما نمی‌توان توسط iTextSharp استفاده کرد؛ اما یک سری برنامه‌ی کمکی برای تبدیل فرمت SVG به مثلا XAML (قابل توجه برنامه نویس‌های WPF و Silverlight) یا WMF و غیره وجود دارد. برای نمونه iTextSharp امکان خواندن فایل‌های WMF را داشته (توسط همان متد معروف Image.GetInstance آن) و اینبار این Image حاصل، یک تصویر برداری است و نه یک Bitmap.
در بین این برنامه‌های تبدیل کننده‌ فرمت‌های برداری، برنامه‌ی معروف و سورس باز Inkscape، در صدر محبوبیت قرار دارد. تنها کافی است فایل SVG خود را در آن گشوده و سپس به انواع و اقسام فرمت‌های دیگر تبدیل (Save As) کنید:



یکی از فرمت‌های جالب خروجی آن، Tex است (مربوط به یک برنامه ادیتور، به نام LaTeX است). فرض کنید یکی از این «چک مارک»های سایت Openclipart را در برنامه Inkscape باز کرده‌ و سپس با فرمت Tex ذخیره کرده‌ایم. خروجی فایل متنی آن مثلا به شکل زیر خواهد بود:

%LaTeX with PSTricks extensions
%%Creator: 0.48.0
%%Please note this file requires PSTricks extensions
\psset{xunit=.5pt,yunit=.5pt,runit=.5pt}
\begin{pspicture}(190,190)
{
\newrgbcolor{curcolor}{0 0 0}
\pscustom[linestyle=none,fillstyle=solid,fillcolor=curcolor]
{
\newpath
\moveto(52.73079005,101.89500456)
\curveto(31.29686559,101.89500456)(13.84575258,84.04652127)(13.8457479,62.12456369)
\curveto(13.8457479,40.20259605)(31.29686559,22.35412714)(52.73079005,22.35412235)
\curveto(74.16470983,22.35412235)(91.6158322,40.20259605)(91.61582751,62.12456369)
\curveto(91.61582751,71.60188248)(88.48023622,80.07729424)(83.15553076,87.02034164)
\lineto(79.49425309,82.58209245)
\curveto(84.13622847,76.73639073)(85.95313131,70.24630402)(85.95313131,62.12456369)
\curveto(85.95313131,43.33817595)(71.09893654,28.1547277)(52.73079005,28.1547277)
\curveto(34.36263419,28.15473249)(19.50844879,43.33817595)(19.50844879,62.12456369)
\curveto(19.50844879,80.91094185)(34.36264355,96.10336589)(52.73079005,96.10336589)
\curveto(58.55122776,96.10336589)(62.90459266,95.2476225)(67.65721002,92.5630926)
\lineto(71.13570481,97.23509821)
\curveto(65.57113223,100.3782653)(59.52269945,101.89500456)(52.73079005,101.89500456)
\closepath
}
}
{
\newrgbcolor{curcolor}{0 0 0}
\pscustom[linestyle=none,fillstyle=solid,fillcolor=curcolor]
{
\newpath
\moveto(38.33889376,67.35513328)
\curveto(39.90689547,67.35509017)(41.09296342,66.03921993)(41.89711165,63.40748424)
\curveto(43.50531445,58.47289182)(44.65118131,56.00562195)(45.33470755,56.0056459)
\curveto(45.85735449,56.00562195)(46.40013944,56.41682961)(46.96305772,57.23928802)
\curveto(58.2608517,75.74384316)(68.7143666,90.71198997)(78.32362116,102.14379168)
\curveto(80.81631349,105.10443984)(84.77658911,106.58480942)(90.20445269,106.58489085)
\curveto(91.49097185,106.58480942)(92.35539361,106.46145048)(92.79773204,106.21480444)
\curveto(93.23991593,105.96799555)(93.4610547,105.65958382)(93.46113432,105.28956447)
\curveto(93.4610547,104.71379041)(92.7976618,103.58294901)(91.47094155,101.89705463)
\curveto(75.95141033,82.81670149)(61.55772504,62.66726353)(48.28984822,41.44869669)
\curveto(47.36506862,39.96831273)(45.47540199,39.22812555)(42.62081088,39.22813992)
\curveto(39.72597184,39.22812555)(38.0172148,39.35149407)(37.49457722,39.5982407)
\curveto(36.12755286,40.2150402)(34.51931728,43.36081778)(32.66987047,49.03557823)
\curveto(30.57914689,55.32711903)(29.53378743,59.27475848)(29.53381085,60.87852533)
\curveto(29.53378743,62.60558406)(30.94099884,64.27099685)(33.75542165,65.87476369)
\curveto(35.48425582,66.86164481)(37.01207517,67.35509017)(38.33889376,67.35513328)
}
}

\end{pspicture}


استفاده از این خروجی در iTextSharp بسیار ساده است. برای مثال:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace HtmlToPdf
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var cb = pdfWriter.DirectContent;

cb.MoveTo(52.73079005f, 101.89500456f);
cb.CurveTo(31.29686559f, 101.89500456f, 13.84575258f, 84.04652127f, 13.8457479f, 62.12456369f);
cb.CurveTo(13.8457479f, 40.20259605f, 31.29686559f, 22.35412714f, 52.73079005f, 22.35412235f);
cb.CurveTo(74.16470983f, 22.35412235f, 91.6158322f, 40.20259605f, 91.61582751f, 62.12456369f);
cb.CurveTo(91.61582751f, 71.60188248f, 88.48023622f, 80.07729424f, 83.15553076f, 87.02034164f);
cb.LineTo(79.49425309f, 82.58209245f);
cb.CurveTo(84.13622847f, 76.73639073f, 85.95313131f, 70.24630402f, 85.95313131f, 62.12456369f);
cb.CurveTo(85.95313131f, 43.33817595f, 71.09893654f, 28.1547277f, 52.73079005f, 28.1547277f);
cb.CurveTo(34.36263419f, 28.15473249f, 19.50844879f, 43.33817595f, 19.50844879f, 62.12456369f);
cb.CurveTo(19.50844879f, 80.91094185f, 34.36264355f, 96.10336589f, 52.73079005f, 96.10336589f);
cb.CurveTo(58.55122776f, 96.10336589f, 62.90459266f, 95.2476225f, 67.65721002f, 92.5630926f);
cb.LineTo(71.13570481f, 97.23509821f);
cb.CurveTo(65.57113223f, 100.3782653f, 59.52269945f, 101.89500456f, 52.73079005f, 101.89500456f);

cb.MoveTo(38.33889376f, 67.35513328f);
cb.CurveTo(39.90689547f, 67.35509017f, 41.09296342f, 66.03921993f, 41.89711165f, 63.40748424f);
cb.CurveTo(43.50531445f, 58.47289182f, 44.65118131f, 56.00562195f, 45.33470755f, 56.0056459f);
cb.CurveTo(45.85735449f, 56.00562195f, 46.40013944f, 56.41682961f, 46.96305772f, 57.23928802f);
cb.CurveTo(58.2608517f, 75.74384316f, 68.7143666f, 90.71198997f, 78.32362116f, 102.14379168f);
cb.CurveTo(80.81631349f, 105.10443984f, 84.77658911f, 106.58480942f, 90.20445269f, 106.58489085f);
cb.CurveTo(91.49097185f, 106.58480942f, 92.35539361f, 106.46145048f, 92.79773204f, 106.21480444f);
cb.CurveTo(93.23991593f, 105.96799555f, 93.4610547f, 105.65958382f, 93.46113432f, 105.28956447f);
cb.CurveTo(93.4610547f, 104.71379041f, 92.7976618f, 103.58294901f, 91.47094155f, 101.89705463f);
cb.CurveTo(75.95141033f, 82.81670149f, 61.55772504f, 62.66726353f, 48.28984822f, 41.44869669f);
cb.CurveTo(47.36506862f, 39.96831273f, 45.47540199f, 39.22812555f, 42.62081088f, 39.22813992f);
cb.CurveTo(39.72597184f, 39.22812555f, 38.0172148f, 39.35149407f, 37.49457722f, 39.5982407f);
cb.CurveTo(36.12755286f, 40.2150402f, 34.51931728f, 43.36081778f, 32.66987047f, 49.03557823f);
cb.CurveTo(30.57914689f, 55.32711903f, 29.53378743f, 59.27475848f, 29.53381085f, 60.87852533f);
cb.CurveTo(29.53378743f, 62.60558406f, 30.94099884f, 64.27099685f, 33.75542165f, 65.87476369f);
cb.CurveTo(35.48425582f, 66.86164481f, 37.01207517f, 67.35509017f, 38.33889376f, 67.35513328f);

cb.SetRGBColorFill(0, 0, 0);
cb.Fill();
}

Process.Start("Test.pdf");
}
}
}

در اینجا، pdfWriter.DirectContent یک Canvas را جهت ترسیمات گرافیکی در اختیار ما قرار می‌دهد. سپس مابقی هم آن مشخص است و یک تناظر یک به یک را می‌شود بین خروجی Tex و متدهای فراخوانی شده، مشاهده کرد. PDF خروجی هم به شکل زیر است:



تا اینجا یک مرحله پیشرفت است. مشکل از اینجا شروع می‌شود که خوب! من که یک «چک مارک» این اندازه‌ای لازم ندارم! آن هم قرار گرفته در پایین صفحه. یک راه حل این مشکل استفاده از متد Transform شیء cb فوق است. این متد یک System.Drawing.Drawing2D.Matrix را دریافت می‌کند و سپس می‌شود توسط آن، اعمال تغییر اندازه (Scale)، تغییر مکان (Translate) و غیره را اعمال کرد. راه دیگر تعریف یک Template از دستورات فوق است. سپس متد Image.GetInstance کتابخانه iTextSharp ورودی از نوع Template را هم قبول می‌کند. خروجی حاصل یک تصویر برداری خواهد بود که اکنون با اکثر اشیاء iTextSharp سازگار است. برای مثال متد سازنده PdfPCell، آرگومان از نوع Image را هم قبول می‌کند. به علاوه شیء Image در اینجا متدهای تغییر اندازه و امثال آن‌را نیز به همراه دارد:

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace HtmlToPdf
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var cb = pdfWriter.DirectContent;
var template = createCheckMark(cb);

var image = Image.GetInstance(template);
image.ScaleAbsolute(40, 40);

var table = new PdfPTable(3);
var cell = new PdfPCell(image)
{
HorizontalAlignment = Element.ALIGN_CENTER
};

for (int i = 0; i < 9; i++)
table.AddCell(cell);

pdfDoc.Add(table);
}

Process.Start("Test.pdf");
}

private static PdfTemplate createCheckMark(PdfContentByte cb)
{
var template = cb.CreateTemplate(140, 140);

template.MoveTo(52.73079005f, 101.89500456f);
template.CurveTo(31.29686559f, 101.89500456f, 13.84575258f, 84.04652127f, 13.8457479f, 62.12456369f);
template.CurveTo(13.8457479f, 40.20259605f, 31.29686559f, 22.35412714f, 52.73079005f, 22.35412235f);
template.CurveTo(74.16470983f, 22.35412235f, 91.6158322f, 40.20259605f, 91.61582751f, 62.12456369f);
template.CurveTo(91.61582751f, 71.60188248f, 88.48023622f, 80.07729424f, 83.15553076f, 87.02034164f);
template.LineTo(79.49425309f, 82.58209245f);
template.CurveTo(84.13622847f, 76.73639073f, 85.95313131f, 70.24630402f, 85.95313131f, 62.12456369f);
template.CurveTo(85.95313131f, 43.33817595f, 71.09893654f, 28.1547277f, 52.73079005f, 28.1547277f);
template.CurveTo(34.36263419f, 28.15473249f, 19.50844879f, 43.33817595f, 19.50844879f, 62.12456369f);
template.CurveTo(19.50844879f, 80.91094185f, 34.36264355f, 96.10336589f, 52.73079005f, 96.10336589f);
template.CurveTo(58.55122776f, 96.10336589f, 62.90459266f, 95.2476225f, 67.65721002f, 92.5630926f);
template.LineTo(71.13570481f, 97.23509821f);
template.CurveTo(65.57113223f, 100.3782653f, 59.52269945f, 101.89500456f, 52.73079005f, 101.89500456f);

template.MoveTo(38.33889376f, 67.35513328f);
template.CurveTo(39.90689547f, 67.35509017f, 41.09296342f, 66.03921993f, 41.89711165f, 63.40748424f);
template.CurveTo(43.50531445f, 58.47289182f, 44.65118131f, 56.00562195f, 45.33470755f, 56.0056459f);
template.CurveTo(45.85735449f, 56.00562195f, 46.40013944f, 56.41682961f, 46.96305772f, 57.23928802f);
template.CurveTo(58.2608517f, 75.74384316f, 68.7143666f, 90.71198997f, 78.32362116f, 102.14379168f);
template.CurveTo(80.81631349f, 105.10443984f, 84.77658911f, 106.58480942f, 90.20445269f, 106.58489085f);
template.CurveTo(91.49097185f, 106.58480942f, 92.35539361f, 106.46145048f, 92.79773204f, 106.21480444f);
template.CurveTo(93.23991593f, 105.96799555f, 93.4610547f, 105.65958382f, 93.46113432f, 105.28956447f);
template.CurveTo(93.4610547f, 104.71379041f, 92.7976618f, 103.58294901f, 91.47094155f, 101.89705463f);
template.CurveTo(75.95141033f, 82.81670149f, 61.55772504f, 62.66726353f, 48.28984822f, 41.44869669f);
template.CurveTo(47.36506862f, 39.96831273f, 45.47540199f, 39.22812555f, 42.62081088f, 39.22813992f);
template.CurveTo(39.72597184f, 39.22812555f, 38.0172148f, 39.35149407f, 37.49457722f, 39.5982407f);
template.CurveTo(36.12755286f, 40.2150402f, 34.51931728f, 43.36081778f, 32.66987047f, 49.03557823f);
template.CurveTo(30.57914689f, 55.32711903f, 29.53378743f, 59.27475848f, 29.53381085f, 60.87852533f);
template.CurveTo(29.53378743f, 62.60558406f, 30.94099884f, 64.27099685f, 33.75542165f, 65.87476369f);
template.CurveTo(35.48425582f, 66.86164481f, 37.01207517f, 67.35509017f, 38.33889376f, 67.35513328f);

template.SetRGBColorFill(0, 0, 0);
template.Fill();

return template;
}
}
}

در این مثال، با کمک متد CreateTemplate مرتبط با Canvas دریافتی، یک قالب جدید ایجاد و سپس روی آن نقاشی خواهیم کرد. اکنون می‌توان از این قالب تهیه شده، یک Image دریافت کرده و سپس مثلا در سلول‌های یک جدول نمایش داد. اینبار خروجی نهایی ما به شکل زیر خواهد بود:



مطالب
آشنایی با NHibernate - قسمت اول

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

چرا نیاز است تا از یک ORM استفاده شود؟
تهیه قسمت و یا لایه دسترسی به داده‌ها در یک برنامه عموما تا 30 درصد زمان کل تهیه یک محصول را تشکیل می‌دهد. اما باید در نظر داشت که این پروسه‌ی تکراری هیچ کار خارق العاده‌ای نبوده و ارزش افزوده‌ی خاصی را به یک برنامه اضافه نمی‌کند. تقریبا تمام برنامه‌های تجاری نیاز به لایه دسترسی به داده‌ها را دارند. پس چرا ما باید به ازای هر پروژه، این کار تکراری و کسل کننده را بارها و بارها تکرار کنیم؟
هدف NHibernate ، کاستن این بار از روی شانه‌های یک برنامه نویس است. با کمک این کتابخانه، دیگر رویه ذخیره شده‌ای را نخواهید نوشت. دیگر هیچگاه با ADO.Net سر و کار نخواهید داشت. به این صورت می‌توان عمده وقت خود را صرف قسمت‌های اصلی و طراحی برنامه کرد تا کد نویسی یک لایه تکراری. همچنین عده‌ای از بزرگان اینگونه ابزارها اعتقاد دارند که برنامه نویس‌هایی که لایه دسترسی به داده‌ها را خود طراحی می‌کنند، مشغول کلاهبرداری از مشتری‌های خود هستند! (صرف زمان بیشتر برای تهیه یک محصول و همچنین وجود باگ‌های احتمالی در لایه دسترسی به داده‌های طراحی شده توسط یک برنامه نویس نه چندان حرفه‌ای)
برای مشاهده سایر مزایای استفاده از یک ORM لطفا به مقاله "5 دلیل برای استفاده از یک ابزار ORM" مراجعه نمائید.

در ادامه برای معرفی این کتابخانه یک سیستم ثبت سفارشات را با هم مرور خواهیم کرد.

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

در این مدل ساده‌ی ما، مشتری‌ها (customers) امکان ثبت سفارشات (orders) را دارند. سفارشات توسط یک کارمند (employee) که مسؤول ثبت آن‌ها است به سیستم وارد می‌شود. هر سفارش می‌تواند شامل یک یا چند (one-to-many) آیتم (order items) باشد و هر آیتم معرف یک محصول (product) است که قرار است توسط یک مشتری (customer) خریداری شود. کلاس دیاگرام این مدل به صورت زیر می‌تواند باشد.


نگاشت مدل

زمانیکه مدل سیستم مشخص شد، اکنون نیاز است تا حالات (داده‌ها) آن‌را در مکانی ذخیره کنیم. عموما اینکار با کمک سیستم‌های مدیریت پایگاه‌های داده مانند SQL Server، Oracle، IBM DB2 ، MySql و امثال آن‌ها صورت می‌گیرد. زمانیکه از NHibernate استفاده کنید اهمیتی ندارد که برنامه شما قرار است با چه نوع دیتابیسی کار کند؛ زیرا این کتابخانه اکثر دیتابیس‌های شناخته شده موجود را پشتیبانی می‌کند و برنامه از این لحاظ مستقل از نوع دیتابیس عمل خواهد کرد و اگر نیاز بود روزی بجای اس کیوال سرور از مای اس کیوال استفاده شود، تنها کافی است تنظیمات ابتدایی NHibernate را تغییر دهید (بجای بازنویسی کل برنامه).
اگر برای ذخیره سازی داده‌ها و حالات سیستم از دیتابیس استفاده کنیم، نیاز است تا اشیاء مدل خود را به جداول دیتابیس نگاشت نمائیم. این نگاشت عموما یک به یک نیست (لزومی ندارد که حتما یک شیء به یک جدول نگاشت شود). در گذشته‌ی نچندان دور کتابخانه‌ی NHibernate ، این نگاشت عموما توسط فایل‌های XML ایی به نام hbm صورت می‌گرفت. این روش هنوز هم پشتیبانی شده و توسط بسیاری از برنامه نویس‌ها بکار گرفته می‌شود. روش دیگری که برای تعریف این نگاشت مرسوم است، مزین سازی اشیاء و خواص آن‌ها با یک سری از ویژگی‌ها می‌باشد که فریم ورک برتر این عملیات Castle Active Record نام دارد.
اخیرا کتابخانه‌ی دیگری برای انجام این نگاشت تهیه شده به نام Fluent NHibernate که بسیار مورد توجه علاقمندان به این فریم ورک واقع گردیده است. با کمک کتابخانه‌ی Fluent NHibernate عملیات نگاشت اشیاء به جداول، بجای استفاده از فایل‌های XML ، توسط کدهای برنامه صورت خواهند گرفت. این مورد مزایای بسیاری را همانند استفاده از یک زبان برنامه نویسی کامل برای تعریف نگاشت‌ها، بررسی خودکار نوع‌های داد‌ه‌ای و حتی امکان تعریف منطقی خاص برای قسمت نگاشت برنامه، به همراه خواهد داشت.

آماده سازی سیستم برای استفاده از NHibernate

در ادامه بجای دریافت پروژه سورس باز NHibernate از سایت سورس فورج، پروژه سورس باز Fluent NHibernate را از سایت گوگل کد دریافت خواهیم کرد که بر فراز کتابخانه‌ی NHibernate بنا شده است و آن‌را کاملا پوشش می‌دهد. سورس این کتابخانه را با checkout مسیر زیر توسط TortoiseSVN می‌توان دریافت کرد.





البته احتمالا برای دریافت آن از گوگل کد با توجه به تحریم موجود نیاز به پروکسی خواهد بود. برای تنظیم پروکسی در TortoiseSVN به قسمت تنظیمات آن مطابق تصویر ذیل مراجعه کنید:



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

پس از دریافت پروژه، باز کردن فایل solution آن در VS‌ و سپس build کل مجموعه، اگر به پوشه‌های آن مراجعه نمائید، فایل‌های زیر قابل مشاهده هستند:

Nhibernate.dll : اسمبلی فریم ورک NHibernate است.
NHibernate.Linq.dll : اسمبلی پروایدر LINQ to NHibernate می‌باشد.
FluentNHibernate.dll : اسمبلی فریم ورک Fluent NHibernate است.
Iesi.Collections.dll : یک سری مجموعه‌های ویژه مورد استفاده NHibernate را ارائه می‌دهد.
Log4net.dll : فریم ورک لاگ کردن اطلاعات NHibernate می‌باشد. (این فریم ورک نیز جهت عملیات logging بسیار معروف و محبوب است)
Castle.Core.dll : کتابخانه پایه Castle.DynamicProxy2.dll است.
Castle.DynamicProxy2.dll : جهت اعمال lazy loading در فریم ورک NHibernate بکار می‌رود.
System.Data.SQLite.dll : پروایدر دیتابیس SQLite است.
Nunit.framework.dll : نیز یکی از فریم ورک‌های بسیار محبوب آزمون واحد در دات نت فریم ورک است.

برای سادگی مراجعات بعدی، این فایل‌ها را یافته و در پوشه‌ای به نام lib کپی نمائید.

برپایی یک پروژه جدید

پس از دریافت Fluent NHibernate ، یک پروژه Class Library جدید را در VS.Net آغاز کنید (برای مثال به نام NHSample1 ). سپس یک پروژه دیگر را نیز از نوع Class Library به نام UnitTests به این solution ایجاد شده جهت انجام آزمون‌های واحد برنامه اضافه نمائید.
اکنون به پروژه NHSample1 ، ارجاع هایی را به فایل‌های FluentNHibernate.dll و سپس NHibernate.dll در که پوشه lib ایی که در قسمت قبل ساختیم، قرار دارند، اضافه نمائید.



در ادامه یک پوشه جدید به پروژه NHSample1 به نام Domain اضافه کنید. سپس به این پوشه، کلاس Customer را اضافه نمائید:

namespace NHSample1.Domain
{
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string PostalCode { get; set; }
public string City { get; set; }
public string CountryCode { get; set; }
}
}
اکنون نوبت تعریف نگاشت این شیء است. این کلاس باید از کلاس پایه ClassMap مشتق شود. سپس نگاشت‌ها در سازنده‌ی این کلاس باید تعریف گردند.

using FluentNHibernate.Mapping;

namespace NHSample1.Domain
{
class CustomerMapping : ClassMap<Customer>
{
}
}
همانطور که ملاحظه می‌کنید، نوع این کلاس Generic ، همان کلاسی است که قصد داریم نگاشت مرتبط با آن را تهیه نمائیم. در ادامه تعریف کامل این کلاس نگاشت را در نظر بگیرید:

using FluentNHibernate.Mapping;

namespace NHSample1.Domain
{
class CustomerMapping : ClassMap<Customer>
{
public CustomerMapping()
{
Not.LazyLoad();
Id(c => c.Id).GeneratedBy.HiLo("1000");
Map(c => c.FirstName).Not.Nullable().Length(50);
Map(c => c.LastName).Not.Nullable().Length(50);
Map(c => c.AddressLine1).Not.Nullable().Length(50);
Map(c => c.AddressLine2).Length(50);
Map(c => c.PostalCode).Not.Nullable().Length(10);
Map(c => c.City).Not.Nullable().Length(50);
Map(c => c.CountryCode).Not.Nullable().Length(2);
}
}
}
به صورت پیش فرض نگاشت‌های Fluent NHibernate از نوع lazy load هستند که در اینجا عکس آن در نظر گرفته شده است.
سپس وضعیت نگاشت تک تک خواص کلاس Customer را مشخص می‌کنیم. توسط Id(c => c.Id).GeneratedBy.HiLo به سیستم اعلام خواهیم کرد که فیلد Id از نوع identity است که از 1000 شروع خواهد شد. مابقی موارد هم بسیار واضح هستند. تمامی خواص کلاس Customer ذکر شده، نال را نمی‌پذیرند (منهای AddressLine2) و طول آن‌ها نیز مشخص گردیده است.
با کمک Fluent NHibernate ، بحث بررسی نوع‌های داده‌ای و همچنین یکی بودن موارد مطرح شده در نگاشت با کلاس اصلی Customer به سادگی توسط کامپایلر بررسی شده و خطاهای آتی کاهش خواهند یافت.

برای آشنایی بیشتر با lambda expressions می‌توان به مقاله زیر مراجعه کرد:
Step-by-step Introduction to Delegates and Lambda Expressions


ادامه دارد...

مطالب دوره‌ها
استفاده از Full Text Search بر روی اسناد XML
امکان استفاده‌ی همزمان قابلیت Full Text Search و اسناد XML ایی نیز در SQL Server پیش بینی شده‌است. به این ترتیب می‌توان متون این اسناد را ایندکس و جستجو کرد. در این حالت تگ‌های XML ایی و ویژگی‌ها، به صورت خودکار حذف شده و در نظر گرفته نمی‌شوند. Syntax استفاده از Full text search در اینجا با سایر حالات و ستون‌های متداول رابطه‌ای SQL Server تفاوتی ندارد. به علاوه امکان ترکیب آن با یک XQuery نیز میسر است. در این حالت، Full text search، ابتدا انجام شده و سپس با استفاده از XQuery می‌توان بر روی این نتایج، نودها، مسیرها و ویژگی‌های خاصی را جستجو کرد.


نحوه‌ی استفاده از Full Text Search بر روی ستون‌های XML ایی

برای  آزمایش، ابتدا یک جدول جدید را که حاوی ستونی XML ایی است، ایجاد کرده و سپس چند سند XML را که حاوی متونی نسبتا طولانی هستند، در آن ثبت می‌کنیم. ذکر CONSTRAINT در اینجا جهت دستور ایجاد ایندکس Full Text Search ضروری است.
CREATE TABLE ftsXML(
id INT IDENTITY PRIMARY KEY,
doc XML NULL
CONSTRAINT UQ_FTS_Id UNIQUE(id)
)
GO
INSERT ftsXML VALUES('
<book>
<title>Sample book title 1</title>
<author>Vahid</author>
<chapter ID="1">
<title>Chapter 1</title>
<content>
"The quick brown fox jumps over the lazy dog" is an English-language 
pangram—a phrase that contains all of the letters of the English alphabet. 
It has been used to test typewriters and computer keyboards, and in other 
applications involving all of the letters in the English alphabet. Owing to its 
brevity and coherence, it has become widely known.
</content>
</chapter>
<chapter ID="2">
<title>Chapter 2</title>
<content>
In publishing and graphic design, lorem ipsum is a placeholder text commonly used 
to demonstrate the graphic elements of a document or visual presentation. 
By replacing the distraction of meaningful content with filler text of scrambled 
Latin it allows viewers to focus on graphical elements such as font, typography, 
and layout.
</content>
</chapter>
</book>
')

INSERT ftsXML VALUES('
<book>
<title>Sample book title 2</title>
<author>Farid</author>
<chapter ID="1">
<title>Chapter 1</title>
<content>
The original passage began: Neque porro quisquam est qui dolorem ipsum quia dolor sit 
amet consectetur adipisci velit 
</content>
</chapter>
<chapter ID="2">
<title>Chapter 2</title>
<content>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor 
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore 
eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, 
sunt in culpa qui officia deserunt mollit anim id est laborum.
</content>
</chapter>
</book>
')
GO
سپس با استفاده از دستورات ذیل، Full text search را بر روی ستون doc جدول ایجاد شده، فعال می‌کنیم:
 CREATE FULLTEXT CATALOG FT_CATALOG
GO
CREATE FULLTEXT INDEX ON ftsXML([doc])
KEY INDEX UQ_FTS_Id ON ([FT_CATALOG], FILEGROUP [PRIMARY])
GO
اکنون می‌توانیم با ترکیبی از امکانات Full Text Search و XQuery، از ستون doc، کوئری‌های پیشرفته و سریعی را تهیه کنیم.


راه اندازی سرویس Full Text Search

البته پیش از ادامه‌ی بحث به کنسول سرویس‌های ویندوز مراجعه کرده و مطمئن شوید که سرویس SQL Full-text Filter Daemon Launcher MSSQLSERVER در حال اجرا است. در غیراینصورت با خطای ذیل مواجه خواهید شد:
 SQL Server encountered error 0x80070422 while communicating with full-text filter daemon host (FDHost) process.
اگر این سرویس در حال اجرا است و باز هم خطای فوق ظاهر شد، مجددا به کنسول سرویس‌های ویندوز مراجعه کرد، در برگه‌ی  خواص سرویس SQL Full-text Filter Daemon Launcher MSSQLSERVER، گزینه‌ی logon را یافته و آن‌را به local system account تغییر دهید و سپس سرویس را ری استارت کنید. پس از آن نیاز است دستور ذیل را نیز اجرا کنید:
 sp_fulltext_service 'restart_all_fdhosts'
go
بعد از اینکار، بازسازی مجدد Full text search را فراموش نکنید. در این حالت در management studio، به بانک اطلاعاتی مورد نظر مراجعه کرده، نود Storage / Full Text Catalog را باز کنید. سپس بر روی FT_CATALOG ایجاد شده در ابتدای بحث کلیک راست کرده و از منوی ظاهر شده، گزینه‌ی Rebuild را انتخاب کنید. در غیراینصورت کوئری‌های ادامه‌ی بحث، خروجی خاصی را نمایش نخواهند داد.


استفاده از متد Contains

در ادامه، نحوه‌ی ترکیب امکانات Full text search و XQuery را ملاحظه می‌کنید:
 -- استفاده از ایکس کوئری برای جستجو در نتایج حاصل
SELECT T.doc.value('(/book/title)[1]', 'varchar(100)') AS title
FROM
-- استفاده از اف تی اس برای جستجو
(SELECT * FROM ftsXML
WHERE CONTAINS(doc, '"Quick Brown Fox "')) AS T
ابتدا توسط متد Contains مرتبط به Full text search، ردیف‌های مورد نظر را یافته و سپس بر روی آن‌ها با استفاده از XQuery جستجوی دلخواهی را انجام می‌دهیم؛ از این جهت که Full text search تنها متون فیلدهای XML ایی را ایندکس می‌کند و نه تگ‌های آن‌ها را.
خروجی کوئری فوق، Sample book title 1 است.

Full text search امکانات پیشرفته‌تری را نیز ارائه می‌دهد. برای مثال در ردیف‌های ثبت شده داریم fox jumps، اما در متن ورودی عبارت جستجو، jumped را وارد کرده و به دنبال نزدیک‌ترین رکورد به آن خواهیم گشت:
 SELECT T.doc.value('(/book/title)[1]', 'varchar(100)') AS title
FROM
(SELECT * FROM ftsXML
WHERE CONTAINS(doc, 'FORMSOF (INFLECTIONAL ,"Quick Brown Fox jumped")')) AS T

و یا دو کلمه‌ی نزدیک به هم را می‌توان جستجو کرد:
 SELECT T.doc.value('(/book/title)[1]', 'varchar(100)') AS title
FROM
(SELECT * FROM ftsXML
WHERE CONTAINS(doc, 'quick NEAR fox')) AS T


نکته‌ای در مورد متد Contains

هم Full text search و هم XQuery، هر دو دارای متدی به نام Contains هستند اما یکی نمی‌باشند.
 SELECT doc.value('(/book/title)[1]', 'varchar(100)') AS title
FROM ftsXML
WHERE doc.exist('/book/chapter/content[contains(., "Quick Brown Fox")]') = 1
در اینجا نحوه‌ی استفاده از متد contains مرتبط با XQuery را مشاهده می‌کنید. اگر این کوئری را اجرا کنید، نتیجه‌ای را دریافت نخواهید کرد. زیرا در ردیف‌ها داریم quick brown fox و نه Quick Brown Fox (حروف ابتدای کلمات، بزرگ نیستند).
بنابراین متد contains مرتبط با XQuery یک جستجوی case sensitive را انجام می‌دهد.
مطالب
بررسی تغییرات Blazor 8x - قسمت سوم - روش ارتقاء برنامه‌های Blazor Server قدیمی به دات نت 8
در قسمت قبل، با نحوه‌ی رندر سمت سرور و روش فعالسازی قابلیت‌های تعاملی در این حالت، آشنا شدیم. از این نکات می‌توان جهت ارتقاء ساختار پروژه‌های قدیمی Blazor Server به Blazor Server 8x استفاده کرد. البته همانطور که پیشتر نیز عنوان شد، در دات نت 8 دیگر خبری از قالب‌های قدیمی پروژه‌های blazor server و blazor wasm نیست و اگر دقیقا همین موارد مدنظر هستند، آن‌ها را می‌توان با تنظیم سطح رندر و میزان تعاملی که مدنظر است، شبیه سازی کرد و یا حتی هر دو را هم با هم در یک پروژه داشت.


1) به‌روز رسانی شماره نگارش دات‌نت

اولین قدم در جهت ارتقاء پروژه‌های قدیمی، تغییر شماره نگارش TargetFramework موجود در فایل csproj. به net8.0 است. پس از اینکار نیاز است تمام بسته‌های نیوگت موجود را نیز به نگارش‌های جدیدتر آن‌ها ارتقاء دهید.


2) فعالسازی حالت SSR تعاملی سمت سرور

پایه‌ی تمام تغییرات انجام شده‌ی در Blazor 8x، قابلیت SSR است و تمام امکانات دیگر برفراز آن اجرا می‌شوند. به همین جهت پس از ارتقاء شماره نگارش دات‌نت، نیاز است SSR را فعال کنیم و برای اینکار باید به هاست ASP.NET Core بگوئیم که درخواست‌های رسیده را به کامپوننت‌های Razor هدایت کند. بنابراین، به فایل Program.cs مراجعه کرده و دو تغییر زیر را به آن اعمال کنید:
// ...
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
// ...
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
یک نمونه‌ی کامل از فایل Program.cs را در قسمت قبل مشاهده کردید و یا حتی می‌توانید دستور dotnet new blazor --interactivity Server را جهت ساخت یک پروژه‌ی آزمایشی جدید بر اساس SDK دات نت 8 و ایده گیری از آن، اجرا کنید.

در اینجا ترکیب کامپوننت‌های تعاملی سمت سرور (AddInteractiveServerComponents) و رندر تعاملی سمت سرور (AddInteractiveServerRenderMode)، دقیقا همان Blazor Server قدیمی است که ما با آن آشنا هستیم.

یک نکته: اگر از قالب جدید dotnet new blazor --interactivity None استفاده کنیم، یعنی حالت تعاملی بودن آن‌را به None تنظیم کنیم، کلیات ساختار پروژه‌ای را که مشاهده خواهیم کرد، با حالت تعاملی Server آن یکی است؛ فقط در تنظیمات Program.cs آن، گزینه‌های فوق را نداریم و به صورت زیر ساده شده‌است:
// ...

builder.Services.AddRazorComponents();

// ...

app.MapRazorComponents<App>();
در این نوع برنامه‌ها نمی‌توان جزایر/قسمت‌های تعاملی Blazor Server را در صفحات و کامپوننت‌های SSR، تعریف کرد. در مورد جزایر تعاملی، در مطالب بعدی بیشتر بحث خواهیم کرد.


3) ایجاد فایل جدید App.razor

در دات نت 8، دیگر خبری از فایل آغازین Host.cshtml_ پروژه‌های Blazor Server قدیمی نیست و کدهای آن با تغییراتی، به فایل جدید App.razor منتقل شده‌اند. در این قسمت، کار هدایت درخواست‌های رسیده به کامپوننت‌های برنامه رخ می‌دهد و از این پس، صفحه‌ی ریشه‌ی برنامه خواهد بود.


در این تصویر، مقایسه‌ای را بین جریان پردازش یک درخواست رسیده در دات نت 8، با نگارش قبلی Blazor Server مشاهده می‌کنید. در دات نت 8، فایل Host.cshtml_ (یک Razor Page آغازین برنامه) با یک کامپوننت Razor به نام App.razor جایگزین شده‌است و فایل قدیمی App.razor این پروژه‌ها به Routes.razor، تغییر نام یافته‌است.

نمونه‌ای از فایل App.razor جدید را که در قسمت قبل نیز معرفی کردیم، در اینجا با جزئیات بیشتری بررسی می‌کنیم:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="MyApp.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>
در این فایل جدید تغییرات زیر رخ داده‌اند:
- تمام دایرکتیوهای تعریف شده مانند page ،@addTagHelper@ و غیره حذف شده‌اند.
- base href تعریف شده اینبار فقط با یک / شروع می‌شود و نه با /~. این مورد خیلی مهم است! اگر به آن دقت نکنید، هیچکدام از فایل‌های استاتیک برنامه مانند فایل‌های css. و js.، بارگذاری نخواهند شد!
- پیشتر برای رندر HeadOutlet، از یک تگ‌هلپر استفاده می‌شد. این مورد در نگارش جدید با یک کامپوننت ساده جایگزین شده‌است.
- تمام component tag helper‌های پیشین حذف شده‌اند و نیازی به آن‌ها نیست.
- ارجاع پیشین فایل blazor.server.js با فایل جدید blazor.web.js جایگزین شده‌است.

یک نکته: همانطور که مشاهده می‌کنید، فایل App.razor یک کامپوننت است و اینبار به همراه تگ <script> نیز شده‌است. یعنی در این نگارش از Blazor می‌توان اسکریپت‌ها را در کامپوننت‌ها نیز ذکر کرد؛ فقط با یک شرط! این کامپوننت حتما باید SSR باشد. اگر این تگ اسکریپتی را در یک کامپوننت تعاملی ذکر کنید، همانند قابل (و نگارش‌های پیشین Blazor) با خطا مواجه خواهید شد.


4) ایجاد فایل جدید Routes.razor و مدیریت سراسری خطاها و صفحات یافت نشده

همانطور که عنوان شد، فایل قدیمی App.razor این پروژه‌ها به Routes.razor تغییر نام یافته‌است که درج آن‌را در قسمت body مشاهده می‌کنید. محتوای این فایل نیز به صورت زیر است:
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
این محتوای جدید، فاقد ذکر کامپوننت NotFound قبلی است؛ از این جهت که سیستم مسیریابی جدید Blazor 8x با ASP.NET Core 8x یکپارچه است و نیازی به این کامپوننت نیست. یعنی اگر قصد مدیریت آدرس‌های یافت نشده‌ی برنامه را دارید، باید همانند ASP.NET Core به صورت زیر در فایل Program.cs عمل کرده:
app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
و سپس کامپوننت جدید StatusCode.razor را برای مدیریت آن به نحو زیر به برنامه اضافه کنید و بر اساس responseCode دریافتی، واکنش‌های متفاوتی را ارائه دهید:
@page "/StatusCode/{responseCode}"

<h3>StatusCode @ResponseCode</h3>

@code {
    [Parameter] public string? ResponseCode { get; set; }
}

یک نکته: اگر پروژه‌ای را بر اساس قالب dotnet new blazor --interactivity Server ایجاد کنیم، در فایل Program.cs آن، چنین تنظیمی اضافه شده‌است:
if (!app.Environment.IsDevelopment())
{
   app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
که دقیقا معادل رفتاری است که در برنامه‌های ASP.NET Core قابل مشاهده‌است. این مسیر Error/، به کامپوننت جدید Components\Pages\Error.razor نگاشت می‌شود. بنابراین اگر در برنامه‌های جدید Blazor Server، استثنائی رخ دهد، با استفاده از میان‌افزار ExceptionHandler فوق، کامپوننت Error.razor نمایش داده خواهد شد.  باید دقت داشت که این کامپوننت ویژه، تحت هر شرایطی در حالت یک static server component رندر می‌شود.

سؤال: در اینجا (برنامه‌های Blazor Server) چه تفاوتی بین UseExceptionHandler و UseStatusCodePagesWithRedirects وجود دارد؟
میان‌افزار UseExceptionHandler برای مدیریت استثناءهای آغازین برنامه، پیش از تشکیل اتصال دائم SignalR وارد عمل می‌شود. پس از آن و تشکیل اتصال وب‌سوکت مورد نیاز، فقط از میان‌افزار UseStatusCodePagesWithRedirects استفاده می‌کند.
اگر علاقمند نیستید تا تمام خطاهای رسیده را همانند مثال فوق در یک صفحه مدیریت کنید، می‌توانید حداقل سه فایل زیر را به برنامه اضافه کنید تا خطاهای متداول یافت نشدن آدرسی، بروز خطایی و یا عدم دسترسی را مدیریت کنند:

404.razor
@page "/StatusCode/404"

<PageTitle>Not found</PageTitle>

<h1>Not found</h1>
<p role="alert">Sorry, there's nothing at this address.</p>

500.razor
@page "/StatusCode/500"

<PageTitle>Unexpected error</PageTitle>

<h1>Unexpected error</h1>
<p role="alert">There was an unexpected error.</p>

401.razor
@page "/StatusCode/401"

<PageTitle>Not Authorized</PageTitle>

<h1>Not Authorized</h1>
<p role="alert">Sorry, you are not authorized to access this page.</p>


5) تعاملی کردن سراسری برنامه

پس از این تغییرات اگر برنامه را اجرا کنید، بر اساس روش جدید static server-side rendering کار می‌کند و تعاملی نیست. یعنی تمام کامپوننت‌های آن به صورت پیش‌فرض، یکبار بر روی سرور رندر شده و خروجی آن‌ها به مرورگر کاربر ارسال می‌شوند و هیچ اتصال دائم SignalR ای برقرار نخواهد شد. برای فعالسازی سراسری قابلیت‌های تعاملی برنامه و بازگشت به حالت Blazor Server قبلی، به فایل App.razor مراجعه کرده و دو تغییر زیر را اعمال کنید تا به صورت خودکار به تمام زیرکامپوننت‌ها، یعنی کل برنامه، اعمال شود:
<HeadOutlet @rendermode="@InteractiveServer" />
...
<Routes @rendermode="@InteractiveServer" />

نکته 1: اجرای دستور زیر در دات‌نت 8، قالب پروژه‌ای را ایجاد می‌کند که رفتار آن همانند پروژه‌های Blazor Server نگارش‌های قبلی دات‌نت است (این مورد را در قسمت قبل بررسی کردیم)؛ یعنی همه‌جای آن به صورت پیش‌فرض، تعاملی است:
dotnet new blazor --interactivity Server --all-interactive

نکته 2: البته ...  InteractiveServer، دقیقا همان حالت پیش‌فرض برنامه‌های Blazor Server قبلی نیست! این حالت رندر، به صورت پیش‌فرض به همراه پیش‌رندر (pre-rendering) هم هست. یعنی در این حالت، روال رویدادگردان OnInitializedAsync یک کامپوننت، دوبار فراخوانی می‌شود (که باید به آن دقت داشت و عدم توجه به آن می‌تواند سبب انجام دوباره‌ی کارهای سنگین آغازین یک کامپوننت شود)؛ یکبار برای پیش‌رندر صفحه به صورت یک HTML استاتیک (بدون فعال سازی هیچ قابلیت تعاملی) که برای موتورهای جستجو و بهبود SEO مفید است و بار دیگر برای فعالسازی قسمت‌های تعاملی آن، درست پس از زمانیکه اتصال SignalR صفحه، برقرار شد (البته امکان فعالسازی حالت پیش‌رندر در Blazor Server قبلی هم وجود داشت؛ ولی مانند Blazor 8x، به صورت پیش‌فرض فعال نبود). در صورت نیاز، برای سفارشی سازی و لغو آن می‌توان به صورت زیر عمل کرد:
@rendermode InteractiveServerRenderModeWithoutPrerendering

@code{
  static readonly IComponentRenderMode InteractiveServerRenderModeWithoutPrerendering = 
        new InteractiveServerRenderMode(false);
}

در مورد پیش‌رندر و روش مدیریت دوبار فراخوانی شدن روال رویدادگردان OnInitializedAsync یک کامپوننت در این حالت، در قسمت‌های بعدی این سری بیشتر بحث خواهد شد.
 
مطالب
انتخاب نوع داده‌ی مناسب مخصوص ذخیره سازی مقادیر پولی در SQL Server
درحال حاضر، باتوجه به خرده نداشتن مقادیر پولی در ایران، عموما از نوع‌های int و bigint برای ذخیره سازی این مقادیر استفاده می‌شود؛ اما در آینده با احتمال حذف تعدادی از صفرها، نیاز به ثبت خرده‌ها هم ضروری خواهد بود و در اینجا این سؤال مهم مطرح می‌شود که نوع داده‌ای مناسب برای انجام اینکار چیست؟ برای نمونه در SQL Server، نوع‌های داده‌ای decimal، money، smallmoney و امثال آن وجود دارند که در این مطلب، تفاوت‌های مهم آن‌ها و روش صحیح انتخاب نوع داده‌ای مناسب مخصوص اینکار را بررسی خواهیم کرد.


مشکل مهم نوع داده‌ای int جهت ذخیره سازی مقادیر پولی

فرض کنید جدول ساده‌ای را با دو فیلد Id و Price دارید که نوع مبلغ آن‌را با توجه به عدم داشتن خرده در واحد پولی، int انتخاب کرده‌اید:
CREATE TABLE [Test1](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Price] [int] NOT NULL,
 CONSTRAINT [PK_Test1] PRIMARY KEY CLUSTERED 
(
[Id] ASC
));
اگر در این جدول فقط 7 رکورد زیر را ثبت کنیم:
 Insert into Test1 values (1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000)
به نظر شما خروجی کوئری ساده‌ی زیر که جهت نمایش جمع مبالغ وارد شده تهیه شده، چیست؟
select sum(price) from Test1
خروجی آن فقط استثنای زیر است!
Arithmetic overflow error converting expression to data type int.
عنوان می‌کند که جمع آن از بازه‌ی اعداد صحیح خارج شده‌است و در سیستمی که نوع مبالغ آن‌را int انتخاب کرده‌اید، دیر یا زود به این مشکل خواهید رسید. فقط کافی است کاربران، یکسالی با آن برنامه کار کنند!
برای حل این مشکل می‌توان به صورت موقت، نوع داده‌ای را به bigint تبدیل کرد و مجددا جمع رکوردها را محاسبه کرد:
select sum(cast(price as bigint)) from Test1
یک روش دیگر مواجه شدن با این مساله، عدم انتخاب نوع int برای فیلد Price، از ابتدای کار است.


از نوع داده‌ای float برای ذخیره سازی مقادیر پولی استفاده نکنید!

هیچگاه نباید از نوع داده‌ی float برای ذخیره سازی مقادیر پولی استفاده کرد؛ از این جهت که این نوع اعداد، به صورت تقریبی از یک مقدار decimal و به صورت باینری در SQL Server ذخیره می‌شوند. به همین جهت به محض ذخیره شدن، با عددی غیر دقیق مواجه خواهیم بود. همچنین مقایسه‌ی دقیق این نوع اعداد هم مشکلات خاصی را به همراه دارد.
DECLARE @f AS FLOAT = '29545428.0211111';
SELECT CAST(@f AS NUMERIC(28, 14)) AS value;



SQL Server چگونه مقادیر پولی money و small money را ذخیره می‌کند؟

SQL Server برای کار با مقادیر پولی، دو نوع MONEY و SMALLMONEY را ارائه می‌دهد که شبیه به نوع‌های BIGINT و INT، نیاز به 8 و 4 بایت برای ذخیره سازی دارند. در عمل نوع MONEY شبیه به نوع DECIMAL(19,4) و نوع SMALLMONEY همانند DECIMAL(10,4) رفتار می‌کند. یعنی نوع MONEY می‌تواند تا 15 رقم دسیمال پیش از ممیز و 4 رقم اعشار را ذخیره کند و نوع SMALLMONEY تنها می‌تواند 6 رقم دسیمال و 4 رقم اعشاری را ذخیره کند.
اما ... هرچند نوع داده‌ی MONEY و DECIMAL(19,4) به ظاهر یکی هستند، اما به نحو متفاوتی بر روی دیسک سخت ذخیره می‌شوند. برای نمونه فرض کنید که قصد داریم عدد 4,513.19 را یکبار به صورت MONEY و بار دیگر به صورت SMALLMONEY ذخیره کنیم که در نهایت به جدول زیر می‌رسیم:


همانطور که مشاهده می‌کنید، نوع‌های MONEY و SMALLMONEY، دقیقا همانند BIGINT هشت بایتی و INT، چهار بایتی ذخیره می‌شوند و عملا در پشت صحنه‌ی SQL Server، اعداد صحیح هستند. اما نوع DECIMAL(19,4) که هرچند شبیه به MONEY عمل می‌کند، 9 بایتی است.


الگوریتم انتخاب نوع داده‌ی مناسب ذخیره سازی مقادیر پولی

در فلوچارت زیر که از کتاب «Donald Knuth’s "The Art of Computer Programming – Volume 1".» انتخاب شده، روش مواجه شدن با انواع و اقسام نوع‌های داده‌ای عددی را به خوبی مشخص می‌کند که آیا عدد در حال ذخیره شدن، خرده دارد یا خیر؟ آیا از 922,337,203,685,477.5807 کوچکتر است یا خیر و امثال آن که در تصمیم‌گیری نهایی مؤثر هستند:


اعدادی را که در این نمودار مشاهده می‌کنید، در جدول زیر بهتر توضیح داده شده‌اند. به عبارتی چه تفاوتی بین نوع Money و Decimal(19,4) مشابه وجود دارد:



تفاوت مهم نوع Money و Decimal(19,4)، در دقت آن‌ها است

 تا اینجا به نظر آنچنان تفاوتی بین نوع Money و Decimal(19,4) وجود ندارد و نوع money اتفاقا یک بایت را کمتر اشغال می‌کند و کوچکتر است. اما تفاوت اصلی را با مثال زیر بهتر می‌توان توضیح داد:
CREATE TABLE MoneyTest (
 Mon1 money,
 Mon2 AS Mon1*Mon1,
 Mon3 AS Mon1*Mon1*Mon1,
 Dec1 decimal(19,4),
 Dec2 AS Dec1*Dec1,
 Dec3 AS Dec1*Dec1*Dec1,
 MonDec AS Mon1*Dec1,
 DecMon AS Dec1*Mon1);
در اینجا جدولی تهیه شده که دو ستون اصلی Mon1 و Dec1 را دارد و مابقی ستون‌های آن، محاسباتی هستند:


همانطور که مشاهده می‌کنید، با ضرب دو عدد دسیمال، مقادیر پیش و پس از ممیز، یعنی precision و scale تغییر کرده‌اند، اما در مورد money چنین چیزی رخ نداده و ثابت است. برای مثال زمانیکه با یک عدد DECIMAL(4,2) کار می‌کنیم، اگر آن‌را ضربدر همین عدد کنیم، به یک عدد DECIMAL(8,4) خواهیم رسید که البته حداکثر precision ممکن آن در SQL Server عدد 38 است، اما یک چنین تغییری در حین ضرب اعداد از نوع money رخ نمی‌دهد.

موضوع دقت را با مثال زیر بهتر می‌توان بررسی کرد:
CREATE TABLE [MoneyTest](
[Id] [int] IDENTITY(1,1) NOT NULL,
decimalMoney decimal(19,4),
moneyMoney money
 CONSTRAINT [PK_MoneyTest] PRIMARY KEY CLUSTERED 
(
[Id] ASC
));
فرض کنید جدولی را داریم با دو فیلد از نوع Money و مشابه آن یعنی decimal(19,4) به صورت فوق. اگر رکوردهای زیر را به آن اضافه کنیم:
INSERT INTO MoneyTest
VALUES
(12321423442.3456,12321423442.3456),
(1111111.1919,1111111.1919)
و سپس سعی کنیم که جمع اعداد وارد شده را محاسبه کنیم:
SELECT * FROM MoneyTest

SELECT SUM(decimalMoney) AS [sumDecimal],
   SUM(moneyMoney) AS [sumMoney]
FROM MoneyTest
به نتیجه‌ی زیر می‌رسیم:


همانطور که مشخص است در حین محاسباتی مانند جمع و منها و محاسبه‌ی sum، تفاوتی بین این نوع‌ها نیست. اما اگر سعی در تقسیم آن‌ها کنیم:
DECLARE @moneyPer money,
  @decimalPer decimal(19,4)
SET @moneyPer = (SELECT moneyMoney FROM MoneyTest WHERE id = 2)/((SELECT moneyMoney FROM MoneyTest WHERE id = 1))
SET @decimalPer = (SELECT decimalMoney FROM MoneyTest WHERE id = 2)/((SELECT decimalMoney FROM MoneyTest WHERE id = 1))
SELECT @moneyPer AS[moneyPer], @decimalPer AS [decimalPer];
به خروجی زیر می‌رسیم:


نتیجه‌ی واقعی 0,00009 است که پس از گرد شدن، به 0.0001 مقدار دسیمال می‌رسیم، اما این دقت در نوع money از دست رفته‌است.

نکته‌ی مهمی که در اینجا قابل مشاهد‌ه‌است، محدود نبودن نتیجه‌ی حاصل، به دقت اعشارها در عدد decimal تعریف شده و scale تعریف شده‌ی اولیه‌ی آن است. نمونه‌ی دیگر آن‌را در مثال زیر می‌توانید مشاهده کنید که هرچند عدد دسیمال تعریف شده، فقط 2 رقم اعشاری دارد، اما در حین تقسیم، از این مساله صرفنظر شده و خروجی آن محدود به 2 رقم اعشار نیست؛ برخلاف نوع money که حداکثر 4 رقم ثابت اعشاری را بیشتر نمی‌تواند داشته باشد:
DECLARE @M MONEY = 1234, @D DECIMAL(6,2) = 1234
SELECT @M/$1000000 AS [MONEY] ,
 @D/$1000000 AS [DECIMAL]



نتیجه‌گیری

برای ذخیره سازی مقادیر پولی در SQL Server، اگر سیستم شما OLTP-like است و با اعدادی مانند 1000.24 کار می‌کنید و حداکثر می‌خواهید جمع و منهای آن‌‌ها را محاسبه کنید، انتخاب نوع  MONEY و یا  SMALLMONEY بسیار مناسب است؛ اما اگر سیستم شما OLAP-like است و در آن اعمال ضرب و تقسیم زیاد رخ می‌دهد، فقط از نوع Decimal استفاده کنید.


DECLARE @dOne DECIMAL(19,4) = 1,
  @dThree DECIMAL(19,4) = 3,
  @mOne MONEY = 1,
  @mThree MONEY = 3

SELECT (@dOne/@dThree) * @dThree AS DecimalResult,
  (@mOne/@mThree) * @mThree AS MoneyResult
مطالب
محدود سازی نرخ دسترسی به منابع در برنامه‌های ASP.NET Core - قسمت دوم - پیاده سازی
در قسمت قبل با مفاهیم، اصطلاحات و الگوریتم‌های مرتبط با میان‌افزار جدید Rate limiting مخصوص ASP.NET Core 7 آشنا شدیم که در پشت صحنه از امکانات موجود در فضای نام System.Threading.RateLimiting استفاده می‌کند. در این قسمت نحوه‌ی استفاده‌ی از آن‌را مرور خواهیم کرد.


روش افزودن میان‌افزار RateLimiter به برنامه‌های ASP.NET Core

شبیه به سایر میان‌افزارها، جهت فعالسازی میان‌افزار RateLimiter، ابتدا باید سرویس‌های متناظر با آن‌را به برنامه معرفی کرد و پس از فعالسازی میان‌افزار مسیریابی، آن‌‌را به زنجیره‌ی مدیریت یک درخواست معرفی نمود. برای نمونه در مثال زیر، امکان دسترسی به تمام درخواست‌ها، به 10 درخواست در دقیقه، محدود می‌شود که پارتیشن بندی آن (در مورد پارتیشن بندی در قسمت قبل بیشتر بحث شد)، بر اساس username کاربر اعتبارسنجی شده و یا hostname یک کاربر غیراعتبارسنجی شده‌است:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
            factory: partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 10,
                QueueLimit = 0,
                Window = TimeSpan.FromMinutes(1)
            }));
});

// ...

var app = builder.Build();

// ...

app.UseRouting();
app.UseRateLimiter();

app.MapGet("/", () => "Hello World!");

app.Run();
توضیحات:
- فراخوانی builder.Services.AddRateLimiter، سبب معرفی سرویس‌های میان‌افزار rate limiter به سیستم تزریق وابستگی‌های ASP.NET Core می‌شود.
- در اینجا می‌توان برای مثال خاصیت options.GlobalLimiter تنظیمات آن‌را نیز مقدار دهی کرد. GlobalLimiter، سبب تنظیم یک محدود کننده‌ی سراسری نرخ، برای تمام درخواست‌های رسیده‌ی به برنامه می‌شود.
- GlobalLimiter را می‌توان با هر نوع PartitionedRateLimiter مقدار دهی کرد که در اینجا از نوع FixedWindowLimiter انتخاب شده‌است تا بتوان «الگوریتم‌های بازه‌ی زمانی مشخص» را به برنامه اعمال نمود تا برای مثال فقط امکان پردازش 10 درخواست در هر دقیقه برای هر کاربر، وجود داشته باشد.
- در پایان کار، فراخوانی app.UseRateLimiter را نیز مشاهده می‌‌کنید که سبب فعالسازی میان‌افزار، بر اساس تنظیمات صورت گرفته می‌شود.

برای آزمایش برنامه، آن‌را  اجرا کرده و سپس به سرعت شروع به refresh کردن صفحه‌ی اصلی آن کنید. پس از 10 بار ریفرش، پیام  503 Service Unavailable را مشاهده خواهید کرد که به معنای مسدود شدن دسترسی به برنامه توسط میان‌افزار rate limiter است.


بررسی تنظیمات رد درخواست‌ها توسط میان‌افزار rate limiter

اگر پس از محدود شدن دسترسی به برنامه توسط میان افزار rate limiter از status code = 503 دریافتی راضی نیستید، می‌توان آن‌را هم تغییر داد:
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = 429;

    // ...
});
برای مثال بسیاری از سرویس‌ها بجای 503، از status code دیگری مانند 429 Too Many Requests استفاده می‌کنند که نحوه‌ی تنظیم آن‌را در مثال فوق مشاهده می‌کنید.
علاوه بر آن در اینجا گزینه‌ی OnRejected نیز پیش بینی شده‌است تا بتوان response ارائه شده را در حالت رد درخواست، سفارشی سازی کرد تا بتوان پیام بهتری را به کاربری که هم اکنون دسترسی او محدود شده‌است، ارائه داد:
builder.Services.AddRateLimiter(options =>
{
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            await context.HttpContext.Response.WriteAsync(
                $"Too many requests. Please try again after {retryAfter.TotalMinutes} minute(s). " +
                $"Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token);
        }
        else
        {
            await context.HttpContext.Response.WriteAsync(
                "Too many requests. Please try again later. " +
                "Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token);
        }
    };

    // ...
});
برای نمونه در مثال فوق ابتدا status code، به 429 تنظیم می‌شود و سپس یک response با معنا به سمت کاربر ارسال می‌گردد که دقیقا مشخص می‌کند آن کاربر چه زمانی می‌تواند مجددا سعی کند و همچنین لینکی را به مستندات محدود سازی برنامه جهت توضیحات بیشتر ارائه می‌دهد.

یک نکته: باتوجه به اینکه در اینجا به HttpContext دسترسی داریم، یعنی به context.HttpContext.RequestServices نیز دسترسی خواهیم داشت که توسط آن می‌توان برای مثال سرویس ILogger را از آن درخواست کرد و رخ‌داد واقع شده را برای بررسی بیشتر لاگ نمود؛ برای مثال چه کاربری مشکل پیدا کرده‌است؟
context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
                .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
                .LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path);
همچنین باید دقت داشت که اگر در اینجا از بانک اطلاعاتی استفاده کرده‌اید، تعداد کوئری‌های آن‌را محدود کنید؛ وگرنه واقعا rate limiter از لحاظ محدود کردن دسترسی به منابع، کمک زیادی را به شما نخواهد کرد.

طراحی فعلی میان‌افزار rate limiter، کمی محدود است. برای مثال «retry after»، تنها metadata مفیدی است که جهت بازگشت ارائه می‌دهد و همچنین مانند GitHub مشخص نمی‌کند که در لحظه‌ی جاری چند درخواست دیگر را می‌توان ارسال کرد و امکان دسترسی به اطلاعات آماری درونی آن وجود ندارد. اگر نیاز به یک چنین اطلاعاتی دارید شاید استفاده از میان‌افزار ثالث دیگری به نام AspNetCoreRateLimit برای شما مفیدتر باشد!


الگوریتم‌های پشتیبانی شده‌ی توسط میان‌افزار rate limiter

در قسمت قبل با چند الگوریتم استاندارد طراحی میان‌افزارهای rate limiter آشنا شدیم که میان‌افزار توکار rate limiter موجود در ASP.NET Core 7x، اکثر آن‌ها را پشتیبانی می‌کند:
- Concurrency limit: ساده‌ترین نوع محدود سازی نرخ درخواست‌ها است و کاری به زمان ندارد و فقط برای آن، تعداد درخواست‌های همزمان مهم است. برای مثال پیاده سازی «مجاز بودن تنها 10 درخواست همزمان».
- Fixed window limit: توسط آن می‌توان محدودیت‌هایی مانند «مجاز بودن تنها 60 درخواست در دقیقه» را اعمال کرد که به معنای امکان ارسال یک درخواست در هر ثانیه در هر دقیقه و یا حتی ارسال یکجای 60 درخواست در یک ثانیه است.
- Sliding window limit: این محدودیت بسیار شبیه به حالت قبل است اما به همراه قطعاتی که کنترل بیشتری را بر روی محدودیت‌ها میسر می‌کند؛ مانند مجاز بودن 60 درخواست در هر دقیقه که فقط در این حالت یک درخواست در هر ثانیه مجاز باشد.
- Token bucket limit: امکان کنترل نرخ سیلان را میسر کرده و همچنین از درخواست‌های انفجاری نیز پشتیبانی می‌کند (این مفاهیم در قسمت قبل بررسی شدند).

علاوه بر این‌ها امکان ترکیب گزینه‌های فوق توسط متد کمکی PartitionedRateLimiter.CreateChained نیز میسر است:
builder.Services.AddRateLimiter(options =>
{
    options.GlobalLimiter = PartitionedRateLimiter.CreateChained(
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
            RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 600,
                    Window = TimeSpan.FromMinutes(1)
                })),
        PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
            RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition =>
                new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 6000,
                    Window = TimeSpan.FromHours(1)
                })));

    // ...
});
برای نمونه در مثال فوق به ازای یک آدرس IP مشخص، تنها می‌توان 600 درخواست را در دقیقه ارسال کرد؛ با این محدودیت که جمع آن‌ها در ساعت، بیشتر از 6000 مورد نباشد.
در این مثال فرضی، متد الحاقی ResolveClientIpAddress اهمیتی ندارد. بهتر است برای برنامه‌ی خود از کلید پارتیشن بندی بهتر و معقول‌تری استفاده کنید.


امکان در صف قرار دادن درخواست‌ها بجای رد کردن آن‌ها

در تنظیمات مثال‌های فوق، در کنار PermitLimit، می‌توان QueueLimit را نیز مشخص کرد. به این ترتیب با رسیدن به PermitLimit، به تعداد QueueLimit، درخواست‌ها در صف قرار می‌گیرند، بجای اینکه کاملا رد شوند:
PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition =>
        new FixedWindowRateLimiterOptions
        {
            AutoReplenishment = true,
            PermitLimit = 10,
            QueueLimit = 6,
            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
            Window = TimeSpan.FromSeconds(1)
        })));
در این مثال هر کلاینت می‌تواند 10 درخواست در ثانیه را ارسال کند. در صورت رسیدن به این محدودیت، تا 6 عدد از درخواست‌های جدید رسیده، بجای رد شدن، در صف قرار می‌گیرند تا در ثانیه‌ی بعدی که این بازه‌ی مشخص به پایان می‌رسد، پردازش شوند.
این تنظیم، تجربه‌ی کاربری بهتری را برای استفاده کنندگان از برنامه‌ی شما به همراه خواهد داشت؛ بجای رد قاطع درخواست‌های ارسالی توسط آن‌ها.

یک نکته: بهتر است QueueLimitهای بزرگی را انتخاب نکنید؛ خصوصا برای بازه‌های زمانی طولانی. چون یک مصرف کننده نیاز دارد تا سریع، پاسخی را دریافت کند و اگر این‌طور نباشد، دوباره سعی خواهد کرد. تنها چند ثانیه‌ی کوتاه در صف بودن برای کاربران معنا دارد.


امکان ایجاد سیاست‌های محدود سازی سفارشی

اگر الگوریتم‌های توکار میان‌افزار rate limiter برای کار شما مناسب نیستند، می‌توانید با پیاده سازی <IRateLimiterPolicy<TPartitionKey، یک نمونه‌ی سفارشی را ایجاد کنید. پیاده سازی این اینترفیس، نیاز به دو متد را دارد:
الف) متد GetPartition که بر اساس HttpContext جاری، یک rate limiter مخصوص را باز می‌گرداند.
ب) متد OnRejected که امکان سفارشی سازی response رد درخواست‌ها را میسر می‌کند.

در مثال زیر پیاده سازی یک rate limiter سفارشی را مشاهده می‌کنید که نحوه‌ی پارتیشن بندی آن بر اساس user-name کاربر اعتبارسنجی شده و یا host-name کاربر وارد نشده‌ی به سیستم است. در اینجا کاربر وارد شده‌ی به سیستم، محدودیت بیشتری دارد:
public class ExampleRateLimiterPolicy : IRateLimiterPolicy<string>
{
    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    {
        if (httpContext.User.Identity?.IsAuthenticated == true)
        {
            return RateLimitPartition.GetFixedWindowLimiter(httpContext.User.Identity.Name!,
                partition => new FixedWindowRateLimiterOptions
                {
                    AutoReplenishment = true,
                    PermitLimit = 1_000,
                    Window = TimeSpan.FromMinutes(1),
                });
        }

        return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(),
            partition => new FixedWindowRateLimiterOptions
            {
                AutoReplenishment = true,
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
            });
    }

    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } =
        (context, _) =>
        {
            context.HttpContext.Response.StatusCode = 418; // I'm a 🫖
            return new ValueTask();
        };
}
و نحوه‌ی معرفی آن به سیستم به صورت زیر است:
options.AddPolicy<string, ExampleRateLimiterPolicy>("myPolicy");


امکان تعریف سیاست‌های محدود سازی نرخ دسترسی به گروهی از endpoints

تا اینجا روش‌های سراسری محدود سازی دسترسی به منابع برنامه را بررسی کردیم؛ اما ممکن است در برنامه‌ای بخواهیم محدودیت‌های متفاوتی را به گروه‌های خاصی از endpoints اعمال کنیم و یا شاید اصلا نخواهیم تعدادی از آن‌ها را محدود کنیم:
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("Api", options =>
    {
        options.AutoReplenishment = true;
        options.PermitLimit = 10;
        options.Window = TimeSpan.FromMinutes(1);
    });

    options.AddFixedWindowLimiter("Web", options =>
    {
        options.AutoReplenishment = true;
        options.PermitLimit = 10;
        options.Window = TimeSpan.FromMinutes(1);
    });

    // ...
});
در این مثال روش تعریف دو سیاست مختلف محدودسازی را مشاهده می‌کنید که اینبار «نامدار» هستند؛ نام یکی Api است و نام دیگری Web.
البته باید درنظر داشت که متدهای الحاقی Add داری را که در اینجا ملاحظه می‌کنید، محدود سازی را بر اساس نام درنظر گرفته شده انجام می‌دهند. یعنی درحقیقت یک محدودسازی سراسری بر اساس گروهی از endpoints هستند و امکان تعریف پارتیشنی را به ازای یک کاربر یا آدرس IP خاص، ندارند. اگر نیاز به اعمال این نوع پارتیشن بندی را دارید، باید از متدهای AddPolicy استفاده کنید:
options.AddPolicy("Api", httpContext =>
        RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(),
        partition => new FixedWindowRateLimiterOptions
        {
            AutoReplenishment = true,
            PermitLimit = 10,
            Window = TimeSpan.FromSeconds(1)
        }));
متدهای AddPolicy دار، هم امکان دسترسی به httpContext جاری را میسر می‌کنند و هم نامدار هستند که قابلیت اعمال آن‌ها را به گروهی از endpoints ممکن می‌کند.


محدود سازی نرخ دسترسی به منابع در ASP.NET Core Minimal API

پس از تعریف نامی برای سیاست‌های دسترسی، اکنون می‌توان از آن‌ها به صورت زیر جهت محدود سازی یک endpoint و یا گروهی از آن‌ها استفاده کرد:
// Endpoint
app.MapGet("/api/hello", () => "Hello World!").RequireRateLimiting("Api");

// Group
app.MapGroup("/api/orders").RequireRateLimiting("Api");
و یا حتی می‌توان بطور کامل محدود سازی نرخ دسترسی را برای یک endpoint و یا گروهی از آن‌ها غیرفعال کرد:
// Endpoint
app.MapGet("/api/hello", () => "Hello World!").DisableRateLimiting();

// Group
app.MapGroup("/api/orders").DisableRateLimiting();


محدود سازی نرخ دسترسی به منابع در ASP.NET Core MVC

می‌توان سیاست‌های نرخ دسترسی تعریف شده را بر اساس نام آن‌ها به کنترلرها و یا اکشن متدها اعمال نمود:
[EnableRateLimiting("Api")]
public class Orders : Controller
{
    [DisableRateLimiting]
    public IActionResult Index()
    {
        return View();
    }

    [EnableRateLimiting("ApiListing")]
    public IActionResult List()
    {
        return View();
    }
}
در اینجا سیاست نرخ دسترسی با نام Api، به کل کنترلر و اکشن متدهای آن اعمال شده، اما اکشن متد Index آن با بکارگیری ویژگی DisableRateLimiting، از این محدودیت خارج و اکشن متد List، از سیاست نام دار دیگری استفاده کرده‌است.
و یا حتی می‌توان این سیاست‌های محدود سازی نرخ دسترسی را به تمام کنترلرها و صفحات razor نیز به صورت زیر اعمال کرد:
app.UseConfiguredEndpoints(endpoints =>
{
    endpoints.MapRazorPages()
        .DisableRateLimiting();

    endpoints.MapControllers()
        .RequireRateLimiting("UserBasedRateLimiting");
});