مطالب
معرفی کتابخانه PdfReport
مدتی هست که در حال تهیه کتابخانه‌ی گزارش سازی مبتنی بر iTextSharp هستم و عمده‌ی استفاده از آن برای من تاکنون، تهیه گزارشات باکیفیت PDF فارسی تحت وب بوده؛ هر چند این کتابخانه وابستگی خاصی به فناوری مورد استفاده ندارد و با WinForms، WPF، مشتقات ASP.NET ، سرویس‌های WCF و کلا هرجایی که دات نت فریم ورک کامل در دسترس باشد، قابل استفاده است. همچنین به منبع داده خاصی گره نخورده است و حتی می‌توانید از یک anonymously typed list عدم متصل به بانک اطلاعاتی خاصی نیز به عنوان منبع داده آن استفاده کنید.
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عامل‌ها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار می‌گیرد و جزئی از آن می‌شود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانه‌های معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاری‌های مرتبط با هر کدام از کتابخانه‌های یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحه‌ای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آن‌ها اشاره‌ای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمن‌های برنامه نویسی مطرح می‌کنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آن‌ها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.


اما چرا PDF؟

استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده می‌شود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینه‌ای می‌توان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونت‌های شکیل‌تر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را می‌توان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عامل‌ها پشتیبانی می‌شود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عامل‌ها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،‌اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت می‌کند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامه‌های رایگان برای مشاهده فایل‌های PDF زیاد است.
- تمام صفحات گزارش را در یک فایل می‌توان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمی‌شود مثلا یک گزارش 100 صفحه‌ای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست می‌دهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آن‌را چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه می‌شود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتاب‌های الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتاب‌ها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین می‌توان به فایل‌های PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامه‌های PDF خوان معتبر گزارش داده شده و می‌توان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایل‌ها و تصاویر قرار داده شده در آن پشتیبانی می‌کند.
- از گرافیک برداری پشتیبانی می‌کند.


مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژه‌ای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده می‌کند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایل‌های اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده می‌کنید، محدودیتی را برای شما ایجاد نخواهد کرد.


کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره می‌توانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.


نحوه تهیه اولین گزارش، با کتابخانه PdfReport

الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژه‌های VS.NET قابل استفاده است، می‌توان از این پروژه Class library به عنوان کلاس‌های پایه قابل استفاده در انواع و اقسام پروژه‌های مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.

ب) آخرین نگارش فایل‌های مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلی‌های موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن می‌باشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.

ج) کلاس‌های زیر را به آن اضافه کنید:
using System.Web;
using System.Windows.Forms;

namespace PdfReportSamples
{
    public static class AppPath
    {
        public static string ApplicationPath
        {
            get
            {
                if (isInWeb)
                    return HttpRuntime.AppDomainAppPath;

                return Application.StartupPath;
            }
        }

        private static bool isInWeb
        {
            get
            {
                return HttpContext.Current != null;
            }
        }
    }
}
از این کلاس برای مشخص سازی محل ذخیره سازی فایل‌های نهایی PDF تولیدی استفاده خواهیم کرد.
همانطور که مشاهده می‌کنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.

در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظه‌ای تعریف خواهیم کرد:
using System;

namespace PdfReportSamples.IList
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string LastName { set; get; }
        public long Balance { set; get; }
        public DateTime RegisterDate { set; get; }
    }
}
اکنون کلاس اصلی گزارش ما به صورت زیر خواهد بود:
using System;
using System.Collections.Generic;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.IList
{
    public class IListPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.ClassicTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 200; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PerviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "رکوردی یافت نشد.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToCsv();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf"));
        }
    }
}
و برای استفاده از آن:
var rpt = new IListPdfReport().CreatePdfReport();
// rpt.FileName


برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثال‌های همراه سورس پروژه در مخزن کد آن مراجعه نمائید.

توضیحات بیشتر:

- در قسمت  DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم می‌شوند.
- سپس نیاز است قلم‌های مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو  قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار می‌گیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص می‌شود. در مورد ساخت Footer سفارشی در قسمت‌های بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمت‌های بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیف‌های گزارش مشخص می‌شود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالب‌‌های سفارشی به مرور در قسمت‌های بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین می‌شود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستون‌های گزارش به کمک متد ColumnsWidthsType مشخص می‌شود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص می‌شود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار می‌گیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمت‌های بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسب‌های جمع‌های پایین صفحات مشخص می‌شود.
- در قسمت MainTableColumns، ستون‌هایی را که علاقمندیم در گزارش ظاهر شوند، قید می‌کنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده می‌کنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستون‌های آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، می‌توان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، می‌توان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجی‌های دلخواه مورد نظر را می‌توان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آن‌ها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.

 لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.
نظرات اشتراک‌ها
تبدیلگر ایران سیستم به یونیکد
این کار رو هم انجام داده بودم ولی باز هم از فایل ایراد میگیره.
مشکل این هست که نرم افزار تامین اجتماعی طبق دستورلعمل خودش کار نکرده. مثلا در راهنما گفته در تاریخ از / استفاده نکنید و در متن عدد ننویسید اما در فایلی که برنامه خودشون میسازه تاریخ‌ها به صورت 94/04/04 هست و در برنامه هم اجازه نوشتن عدد در متن رو میده و در فایل هم در متن‌ها عدد هست.
شاید در تبدیل یونیکد به ایران سیستم جایی اشتباه هست. عددها رو که نیاز هست که به ایران سیستم تبدیل کنم؟ من فقط متن‌ها رو تبدیل کردم و عددها رو مستقیم در dbf نوشتم.
نظرات اشتراک‌ها
معرفی کامپوننت DNTCaptcha.Blazor
ممنون بابت پاسخ . من دقیقا به همین شکل عمل کردم EditForm و کد مربوطه با حذف کد‌های بررسی کاربری را ضمیمه کردم اما پاسخ سوال امنیتی به عنوان Validate عمل نمی‌کنه و نکته بعدی Refresh شدن هم با تغییر Bind-Value که گفته بودید انجام نمی‌شه نمی‌دونم مشکل از کجاست چون من موازی از این کامپوننت روی دو تا پروژه استفاده کردم نیاز دارم که یکبار همه این مواردش رو بتونم انجام بدم . تشکر از شما


مطالب
پیاده سازی گروه بندی ایمیل‌های ارسالی یا message threading
اگر به ایمیل‌های ارسالی از طرف GitHub دقت کرده باشید، کلاینت‌های دریافت ایمیل‌ها، تمام ایمیل‌های مرتبط با یک Issue موجود را، در ذیل هم نمایش می‌دهند و بجای اینکه چند 10 ایمیل ارسالی را به نحوی جداگانه نمایش دهند، برای خلوت‌تر کردن نحوه‌ی نمایش ایمیل‌های رسیده و کاهش نویز، آن‌ها را تنها در طی یک ایمیل ارائه می‌کنند:


برای نمونه در اینجا کل موضوع مرتبط با ELMAH، تنها در طی یک ایمیل نمایش داده می‌شود و هرچند 13 ایمیل، مرتبط با آن هستند، اما 13 ایمیل به صورت جداگانه نمایش داده شده را دریافت نمی‌کنیم. علت این موضوع به Header خاص این نوع ایمیل‌ها بر می‌گردد:
 From: Atif Aziz <notifications@github.com>
Reply-To: elmah/Elmah <reply+000bb03ad52eb40a4ec2d49bf78c53c3eba42efc401701a592cf00000001143e18c892a169ce0ae0bf4c@reply.github.com>
To: elmah/Elmah <Elmah@noreply.github.com>
Message-ID: <elmah/Elmah/issues/407/260080923@github.com>
In-Reply-To: <elmah/Elmah/issues/407@github.com>
References: <elmah/Elmah/issues/407@github.com>
Subject: Re: [elmah/Elmah] Will ELMAH be ported to ASP.NET Core? (#407)
در اینجا هدرهای استاندارد (RFC 5322) و ویژه‌ی Message-ID، In-Reply-To و References هستند که سبب فعال شدن گروه بندی ایمیل‌های ارسالی یا message threading در کلاینت‌های دریافت و نمایش ایمیل‌ها می‌شوند و فرمت کلی آن‌ها به صورت <ID@HOST> است.
Message-ID بیانگر شماره‌ی منحصربفرد ایمیل ارسالی است.
فیلدهای اختیاری In-Reply-To و References تنها زمانی ذکر می‌شوند که قصد ارسال پاسخی، به یک Message-ID خاص، وجود داشته باشد. بنابراین مقدار درج شده‌ی در آن‌ها دقیقا باید معادل Message-ID ایی باشد که پیشتر ارسال شده‌است.
اگر تنها فیلد References ذکر شود، از آن جهت تشخیص گروه یا Thread ایمیل‌های رسیده استفاده می‌شود.
اگر نیاز به ذکر بیش از یک Message-ID وجود داشته باشد، نحوه‌ی درج آن به صورت ذیل است:
 References:
<11111@yoursite.com>
<22222@yoursite.com>
<33333@yoursite.com>


نحوه‌ی پیاده سازی این قابلیت توسط SmtpClient دات نت

در کدهای ذیل نحوه‌ی افزودن هدرهای یاد شده را توسط SmtpClient دات نت مشاهده می‌کنید:
var smtpClient = new SmtpClient("….",587);

using (MailMessage message = new MailMessage("USERNAME@gmail.com","USERNAME@gmail.com"))
{
   message.Subject = "test";
   message.Headers.Add("Message-ID", "<MESSAGEID@site.com>");   
   smtpClient.Send(message);
}

using (MailMessage message = new MailMessage("USERNAME@gmail.com","USERNAME@gmail.com"))
{
   message.Subject = "Re: test";
   message.Headers.Add("In-Reply-To", "<MESSAGEID@site.com>");
   message.Headers.Add("References",  "<MESSAGEID@site.com>");
   smtpClient.Send(message);
}
ابتدا یک ایمیل معمولی ارسال شده‌است؛ با این تفاوت که هدر جدید Message-ID را به آن افزوده‌ایم.
از این ID در ایمیل‌های بعدی جهت ارجاع به آن و نمایش Thread مانند آن‌ها، به کمک فیلدهای In-Reply-To و References، استفاده خواهیم کرد.

برای مثال هدر اطلاع رسانی شروع یک بحث جدید به صورت ذیل است:
message.Headers.Add("Message-ID", $"<post/{post.id}@your-app-name.example>");
و سپس نظری که برای آن ارسال می‌شود، چنین هدرهایی را خواهد داشت:
message.Headers.Add("Message-ID", $"<comments/{comment.id}@your-app-name.example>");
message.Headers.Add("In-Reply-To", $"<post/{post.id}@your-app-name.example>");
message.Headers.Add("References", $"<post/{post.id}@your-app-name.example>");
مطالب
حذف اعراب از حروف و کلمات
برای بهبود قسمت ثبت نام در یک سایت بهتر است بین «وحید» و «وَحید» تفاوتی قائل نشد. این مورد ممکن است خصوصا حین ارسال پیام‌های خصوصی در آینده جهت تشخیص افراد مشکل ساز شود. همچنین در تهیه slug برای نمایش در Urlها نیز باید اعراب را حذف کرد. منظور از slug، عنوان کوتاهی است که در انتهای یک آدرس ممکن است ذکر شود.
 http://www.site.com/post/12/slug

سؤال: چگونه می‌توان اعراب را از متون فارسی یا عربی حذف کرد؟

متد انجام اینکار را در ذیل مشاهده می‌کنید:
using System.Globalization;
using System.Text;

    static string RemoveDiacritics(string text)
    {
        var normalizedString = text.Normalize(NormalizationForm.FormD);
        var stringBuilder = new StringBuilder();

        foreach (var c in normalizedString)
        {
            var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
            if (unicodeCategory != UnicodeCategory.NonSpacingMark)
            {
                stringBuilder.Append(c);
            }
        }

        return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
    }

توضیحات

متد Normalize با پارامتر NormalizationForm.FormD، سبب می‌شود تا کاراکترها به گلیف‌های اصلی تشکیل دهنده‌ی آن‌ها تجزیه شوند. به عبارتی، حروف از اعراب جدا خواهند شد. در ادامه این کاراکترها اسکن شده و صرفا مواردی که حروف پایه را تشکیل می‌دهند، جمع آوری و بازگشت داده می‌شوند. حالت NormalizationForm.FormC که در انتها بکار گرفته شده، برعکس است.
در یونیکد یک حرف می‌تواند از یک یا چند code point تشکیل شود. در حالت FormC، هر حرف با اعراب آن یک code point را تشکیل می‌دهند. در حالت FormD، حرف با اعراب آن دو code point را تشکیل خواهند داد. به همین جهت در ابتدای کار، رشته تبدیل به حالت D شده تا بتوان اعراب آن‌را مجزای از حروف پایه حذف کرد.
البته اعراب در اینجا به اعراب عربی ختم نمی‌شود. یک سری حروف اروپایی مانند  "ä" ،"ö" و "ü" را نیز شامل می‌شود.
نظرات مطالب
استفاده از DbProviderFactory
من توصیه می‌کنم که ADO.NET رو به شکل خام آن فراموش کنید. این نوع روش‌ها هرچند پایه و اساس تمام ORMهای نوشته شده هستند، اما فقط ابتدای کار را به شما نشان می‌دهند. واقعیت این است که سوئیچ کردن بین بانک‌های اطلاعاتی مختلف نیاز به تولید SQL قابل فهم برای آن موتور خاص را نیز دارد. اینجا است که ORMها در وقت شما صرفه جویی می‌کنند. شما کوئری LINQ می‌نویسید اما در پشت صحنه بر اساس پروایدر مورد استفاده، این کوئری LINQ به معادل SQL قابل فهم برای بانک اطلاعاتی مورد نظر ترجمه می‌شود. خیلی از توابع هستند که در بانک‌های اطلاعاتی مختلف تفاوت می‌کنند و این SQL ایی که مورد بحث است ... در عمل آنچنان استاندارد نیست. توابع تاریخ در SQLite با SQL Server فرق می‌کند. نوع‌های داده‌ای این‌ها عموما تطابق ندارد و مسایل دیگر. ORMها می‌توانند این مسایل را به خوبی مدیریت کنند بدون اینکه شما آنچنان درگیر این جزئیات شوید.
مطالب
تبدیل قالب‌های سفارشی پروژه‌های NET Core. به بسته‌های NuGet
در مطلب «امکان ساخت قالب برای پروژه‌های NET Core.» با مقدمات تبدیل یک پروژه‌ی سفارشی سازی شده، به یک قالب ایجاد پروژه‌های جدید NET Core. آشنا شدیم. اگر علاقمند باشید می‌توانید قالب‌های خود را به صورت بسته‌های نیوگت نیز با دیگران به اشتراک بگذارید. برای نمونه تمام قالب‌هایی را که توسط دستور dotnet new قابل نصب هستند، می‌توانید در مسیر ذیل، در سیستم خود پیدا کنید:
 %userprofile%\.templateengine\dotnetcli


و یا قالبی را که در قسمت قبل، به سیستم dotnet new اضافه کردیم، مدخل تعریف آن، در فایل templatecache.json ذیل، ثبت شده‌است (short name آن‌را در این فایل جستجو کنید):
 %userprofile%\.templateengine\dotnetcli\v2.0.0-preview2-006497\templatecache.json

برای حذف قالب تعریف شده از سیستم dotnet new،  تنها دستور ذیل وجود دارد که سبب حذف تعریف تمام قالب‌های سفارشی جدید می‌شود:
 dotnet new --debug:reinit


ساخت بسته‌ی نیوگت از قالب سفارشی


- برای ساخت بسته‌ی نیوگت، ابتدا یک پوشه‌ی مجزا را خارج از پروژه‌ی خود ایجاد کنید (تصویر فوق).
- سپس آخرین نگارش فایل nuget.exe را از آدرس https://dist.nuget.org/index.html دریافت کنید و به داخل این پوشه کپی نمائید.
- فایل pack.bat دارای این یک سطر است:
 nuget pack .\nuget\Templates.nuspec
کار آن پردازش فایل Templates.nuspec و تولید بسته‌ی نیوگت متناظر با آن است.


- در ادامه داخل پوشه‌ی nuget، مطابق تصویر فوق، پوشه‌ای به نام Content و فایل خالی Templates.nuspec را ایجاد کنید.
پوشه‌ی Content در برگیرنده‌ی قسمتی از پروژه‌است که قرار است درون بسته‌ی نیوگت قرارگیرد (یعنی تمام فایل‌های پروژه به همراه پوشه‌ی مخصوص template.config. باید به اینجا کپی شوند). برای مثال پوشه‌های Bin و Obj و یا اسکریپت‌های جانبی را می‌شود در اینجا لحاظ نکرد.
- محتوای فایل Templates.nuspec یک چنین ساختاری را دارد:
<?xml version="1.0" encoding="utf-8"?>
<package 
    xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
    <metadata>
        <id>DNT.Identity</id>
        <version>1.0.0</version>
        <description>Empty DNT.Identity project</description>
        <authors>VahidN (https://www.dntips.ir/)</authors>
        <language>en-US</language>
        <projectUrl>https://github.com/VahidN/DNTIdentity</projectUrl>
        <licenseUrl>https://github.com/VahidN/DNTIdentity/blob/master/LICENSE.md</licenseUrl>
        <packageTypes>
            <packageType name="Template" />
        </packageTypes>
    </metadata>
</package>
که در آن، نام، شماره و توضیحاتی در مورد پروژه ذکر می‌شوند. همچنین نوع این بسته به Template تنظیم خواهد شد.

- اکنون فایل pack.bat را اجرا کنید. پس از آن فایلی مانند DNT.Identity.1.0.0.nupkg تولید خواهد شد که آن‌را می‌توان در سایت nuget.org مانند سایر بسته‌های نیوگت آپلود کرد و به اشتراک گذاشت.

یک نکته: می‌شد فایل nuget.exe و pack.bat را در کنار پوشه‌ی Content و فایل Templates.nuspec هم قرار داد. در این حالت پس از اجرای دستور nuget pack، فایل‌های exe و bat نیز داخل فایل نهایی تولیدی قرار می‌گرفتند. بنابراین بهتر است این‌ها را درون یک پوشه قرار نداد.


نحوه‌ی نصب یک قالب جدید پروژه‌های NET Core. از طریق نیوگت

پس  از آپلود فایل nupkg حاصل در سایت nuget.org، اکنون نحوه‌ی نصب آن در سیستم به صورت ذیل است:
در حالت عمومی:
 dotnet new --install [name]::[version]
و یا در اینجا:
 dotnet new --install DNT.Identity::*
*:: به معنای نصب آخرین نگارش موجود است.


همانطور که در تصویر فوق نیز ملاحظه می‌کنید، این قالب جدید در کنار سایر قالب‌های پیش‌فرض SDK مربوط به NET Core. قرار گرفته‌است.

اکنون کار با این قالب نصب شده، همانند قسمت «نحوه‌ی ایجاد یک پروژه‌ی جدید بر اساس قالب نصب شده» مطلب پیشین است:
 dotnet new dntidentity -n MyNewProj
مطالب
تبدیل عدد به حروف

به طور قطع توابع و کلاس‌های تبدیل عدد به حروف، در جعبه ابزار توابع کمکی شما هم پیدا می‌شوند. روز قبل سعی کردم جهت آزمایش، عدد 3000,000,000,000,000 ریال را با کلاسی که دارم تست کنم و نتیجه overflow یا اصطلاحا ترکیدن سیستم بود! البته اگر مطالب این سایت را دنبال کرده باشید پیشتر در همین راستا مطلبی در مورد نحوه‌ی صحیح بکارگیری توابع تجمعی SQL در این سایت منتشر شده است و جزو الزامات هر سیستمی است (تفاوتی هم نمی‌کند که به چه زبانی تهیه شده باشد). اگر آ‌ن‌را رعایت نکرده‌اید، سیستم شما «روزی» دچار overflow خواهد شد.

در کل این کلاس تبدیل عدد به حروف را به صورت ذیل اصلاح کردم و همچنین دو زبانه است؛ چیزی که کمتر در پیاده سازی‌های عمومی به آن توجه شده است:

using System.Collections.Generic;
using System.Linq;

namespace NumberToWordsLib
{
/// <summary>
/// Number to word languages
/// </summary>
public enum Language
{
/// <summary>
/// English Language
/// </summary>
English,

/// <summary>
/// Persian Language
/// </summary>
Persian
}

/// <summary>
/// Digit's groups
/// </summary>
public enum DigitGroup
{
/// <summary>
/// Ones group
/// </summary>
Ones,

/// <summary>
/// Teens group
/// </summary>
Teens,

/// <summary>
/// Tens group
/// </summary>
Tens,

/// <summary>
/// Hundreds group
/// </summary>
Hundreds,

/// <summary>
/// Thousands group
/// </summary>
Thousands
}

/// <summary>
/// Equivalent names of a group
/// </summary>
public class NumberWord
{
/// <summary>
/// Digit's group
/// </summary>
public DigitGroup Group { set; get; }

/// <summary>
/// Number to word language
/// </summary>
public Language Language { set; get; }

/// <summary>
/// Equivalent names
/// </summary>
public IList<string> Names { set; get; }
}

/// <summary>
/// Convert a number into words
/// </summary>
public static class HumanReadableInteger
{
#region Fields (4)

private static readonly IDictionary<Language, string> And = new Dictionary<Language, string>
{
{ Language.English, " " },
{ Language.Persian, " و " }
};
private static readonly IList<NumberWord> NumberWords = new List<NumberWord>
{
new NumberWord { Group= DigitGroup.Ones, Language= Language.English, Names=
new List<string> { string.Empty, "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }},
new NumberWord { Group= DigitGroup.Ones, Language= Language.Persian, Names=
new List<string> { string.Empty, "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" }},

new NumberWord { Group= DigitGroup.Teens, Language= Language.English, Names=
new List<string> { "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" }},
new NumberWord { Group= DigitGroup.Teens, Language= Language.Persian, Names=
new List<string> { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" }},

new NumberWord { Group= DigitGroup.Tens, Language= Language.English, Names=
new List<string> { "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }},
new NumberWord { Group= DigitGroup.Tens, Language= Language.Persian, Names=
new List<string> { "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" }},

new NumberWord { Group= DigitGroup.Hundreds, Language= Language.English, Names=
new List<string> {string.Empty, "One Hundred", "Two Hundred", "Three Hundred", "Four Hundred",
"Five Hundred", "Six Hundred", "Seven Hundred", "Eight Hundred", "Nine Hundred" }},
new NumberWord { Group= DigitGroup.Hundreds, Language= Language.Persian, Names=
new List<string> {string.Empty, "یکصد", "دویست", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد" , "نهصد" }},

new NumberWord { Group= DigitGroup.Thousands, Language= Language.English, Names=
new List<string> { string.Empty, " Thousand", " Million", " Billion"," Trillion", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
new NumberWord { Group= DigitGroup.Thousands, Language= Language.Persian, Names=
new List<string> { string.Empty, " هزار", " میلیون", " میلیارد"," تریلیون", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
};
private static readonly IDictionary<Language, string> Negative = new Dictionary<Language, string>
{
{ Language.English, "Negative " },
{ Language.Persian, "منهای " }
};
private static readonly IDictionary<Language, string> Zero = new Dictionary<Language, string>
{
{ Language.English, "Zero" },
{ Language.Persian, "صفر" }
};

#endregion Fields

#region Methods (7)

// Public Methods (5) 

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this int number, Language language)
{
return NumberToText((long)number, language);
}


/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this uint number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this byte number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this decimal number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this double number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this long number, Language language)
{
if (number == 0)
{
return Zero[language];
}

if (number < 0)
{
return Negative[language] + NumberToText(-number, language);
}

return wordify(number, language, string.Empty, 0);
}
// Private Methods (2) 

private static string getName(int idx, Language language, DigitGroup group)
{
return NumberWords.Where(x => x.Group == group && x.Language == language).First().Names[idx];
}

private static string wordify(long number, Language language, string leftDigitsText, int thousands)
{
if (number == 0)
{
return leftDigitsText;
}

var wordValue = leftDigitsText;
if (wordValue.Length > 0)
{
wordValue += And[language];
}

if (number < 10)
{
wordValue += getName((int)number, language, DigitGroup.Ones);
}
else if (number < 20)
{
wordValue += getName((int)(number - 10), language, DigitGroup.Teens);
}
else if (number < 100)
{
wordValue += wordify(number % 10, language, getName((int)(number / 10 - 2), language, DigitGroup.Tens), 0);
}
else if (number < 1000)
{
wordValue += wordify(number % 100, language, getName((int)(number / 100), language, DigitGroup.Hundreds), 0);
}
else
{
wordValue += wordify(number % 1000, language, wordify(number / 1000, language, string.Empty, thousands + 1), 0);
}

if (number % 1000 == 0) return wordValue;
return wordValue + getName(thousands, language, DigitGroup.Thousands);
}

#endregion Methods
}
}



دریافت پروژه کامل به همراه Unit tests مرتبط


مطالب
پیاده سازی Load Balancing در Nginx

زمانیکه تعداد کاربران سایت بیشتر میشود و ترافیک سایت افزایش می‌یابد عموما یک نمونه ( instance ) از برنامه نمیتواند پاسخگوی همه درخواست‌ها باشد و مجبور هستیم چندین نمونه برنامه را بر روی چند سرور اجرا کنیم. با این حال نیازمند یک وب سرور هستیم که درخواست‌های ارسال شده را در بین instance های موجود پخش کند. با انجام این کار تعداد ریکوئست‌ها در بین instance ها تقسیم میشوند. برای انجام اینکار میتوانیم از nginx استفاده کنیم. nginx قابلیت load balancing را دارد. برای پیاده سازی load balancing در nginx باید در فایل nginx.conf یک گروه از نوع upstream ایجاد کنید در کانتکست http.
http {
    upstream backend {
        server backend1.example.com;
        server backend2.example.com;
        server backend3.example.com;
    }
}
در کانفیگ بالا یک گروه به نام backend ایجاد کرده‌ایم که شامل سه سرور می‌باشد. یعنی سه instance از برنامه در حال اجرا هستند و میتوانیم ریکوئست‌های ارسال شده را بر روی این سرور‌ها ارسال کنیم. سپس باید در کانتکس server ( این کانتکست هم درون کانتکست http قرار دارد ) یک بلاک از نوع location ایجاد کنیم. بعد از کلمه location باید مسیری را که درخواست‌ها به آن ارسال میشوند را بنویسید که در مثال پایین همان روت وب سایت نوشته شده است ( / ). نوشتن / بعد از location تمامی درخواست‌ها را شامل میشود. همچنین میتوانید فقط درخواست‌هایی را که به api ها ارسال میشوند، فیلتر کنید که میتوانید به جای / از api/ استفاده کنید. با این کار تمامی درخواست‌هایی که url آنها با api/ شروع شود وارد بلاک location میشوند.
سپس برای ارسال کردن درخواست‌ها در بین برنامه‌های در حال اجرا باید از proxy_pass استفاده کنیم. بعد از کلمه proxy_pass باید نام upstream ی را که در بالا نوشته‌ایم، وارد کنیم. با انجام اینکار تمامی درخواست‌های ارسال شده به / در بین سرور‌های backend پخش میشوند.
http {
    upstream backend {
        server backend1.example.com;
        server backend2.example.com;
        server backend3.example.com;
    }
    
    server {
        location / {
            proxy_pass http://backend;
        }
    }
}
وب سرور nginx به طور پیشفرض از الگوریتم Round robin برای پخش کردن درخواست‌ها در بین سرور‌ها استفاده میکند. البته میتوانیم این الگوریتم را تغییر بدهیم. به طور مثال میتوانیم مشخص کنیم که به هرکدام از سرور‌ها چند درخواست ارسال شود.
با افزودن weight در کنار نام سرور‌ها مشخص میکنم که به طور مثال اگر 7 درخواست به سرور ارسال شود، 5 مورد به backend1 ارسال میشود و 2 مورد به backend2. همچنین میتوانیم مشخص کنیم که یک سرور به عنوان سرور backup باشد مانند backend4.
اگر سروری در دسترس نباشد میتوانید با اضافه کردن کلمه down بعد از نام سرور، از ارسال درخواست به آن جلوگیری کنید؛ مانند backend3 و یا قبل از کلمه server یک علامت # قرار دهید.
    upstream backend {
        server backend1.example.com weight=5;
        server backend2.example.com weight=2;
        server backend3.example.com down;
        server backend4.example.com backup;
    }
با انجام این کار سرور backend4 به عنوان سرور backup معرفی شده است و تا زمانیکه ما بقی سرورها در دسترس باشند، هیچ ریکوئستی به سرور backend4 ارسال نمیشود. اگر تمامی سرور‌های backend1 و backend2 از دسترس خارج شوند، آنگاه nginx درخواست‌های ارسال شده را به سرور backup یعنی backend4 ارسال میکند. البته به طور پیشفرض nginx سلامت سرورها را بررسی نمیکند و باید یک سری تنظیمات مربوط به health_check را ثبت کنیم. با اضافه کردن دستور health_check در پایین proxy_pass وب سرور nginx هر 5 ثانیه یک بار یک ریکوئست را به سرورهای backend ارسال میکند و اگر هرکدام از سرور‌ها ریسپانس کدی خارج از کدهای 200 تا 399 را ارسال کنند، درخواست‌های بعدی را به آن سرورها ارسال نمیکند.
server {
    location / {
        proxy_pass   http://backend;
        health_check;
    }
}
در کنار دستور health_check میتوانیم یک سری پارامتر را هم مشخص کنیم. به طور مثال میتوانیم مشخص کنیم که درخواست health_check را به چه uri ای ارسال کند و یا به چه پورتی درخواست را ارسال کند.
server {
    location / {
        proxy_pass   http://backend;
        health_check port=8080;
       #health_check uri=/healthcheck;
    }
}
(برای کامنت کردن یک دستور از # استفاده میشود).
همچنین میتوانیم یک درخواست سفارشی را برای مشخص کردن health_check ایجاد کنیم:
http {
    #...
    match welcome {
        status 200;
        header Content-Type = text/html;
        body ~ "Welcome to nginx!";
    }
    server {
        #...
        location / {
            proxy_pass http://backend;
            health_check match=welcome;
        }
    }
}
در کانفیگ بالا مشخص کرده‌ایم که برای نشان دادن در دسترس بودن هرکدام از سرورها باید استتوس کد 200 برگردانده شود و body دریافت شده حاوی !Welcome to nginx باشد.
نکته : match و health_check در نسخه تجاری nginx قابل استفاده می‌باشند.
نظرات مطالب
سیلورلایت 5 و تاریخ شمسی
موضوع بحث کاملا مشخص است. تاریخ شمسی و سیلورلایت 5.
یافتن Max با LINQ یک نکته انحرافی هم دارد. زمانیکه شما OrderByDescending را فراخوانی کنید و سپس Take اولین المان لیست، به همان Max می‌‌رسید.
var max = query.OrderByDescending(...) .Take(1);