مطالب
سفارشی سازی Header و Footer در PdfReport
صورت مساله:
- می‌خواهیم footer پیش فرض PdfReport را که تاریخ را در یک سمت، و شماره صفحه را در سمتی دیگر نمایش می‌دهد، به عبارت «صفحه x از n» تغییر دهیم.
- می‌خواهیم در Header گزارش بجای Header پیش فرض PdfReport یکی از قالب‌های PDF تهیه شده توسط Open Office را نمایش دهیم (و یا هر ساختار دیگری را).

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

مدل‌های مورد استفاده:
namespace PdfReportSamples.Models
{
    public class Task
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public int PercentCompleted { set; get; }
        public bool IsActive { set; get; }
        public User Assignee { set; get; }
    }
}

using System;

namespace PdfReportSamples.Models
{
    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; }
    }
}
توسط این مدل‌ها قصد داریم تعدادی فعالیت (Task) را که به تعدادی کاربر انتساب یافته است، نمایش دهیم. همچنین نمایش مقادیر خواص تو در تو  نیز در اینجا مد نظر است؛ برای مثال ستونی مانند این:
 column.PropertyName<Task>(x => x.Assignee.Name) 
کدهای کامل مثال را در ادامه ملاحظه خواهید نمود:
using System;
using System.Collections.Generic;
using System.Drawing;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.CustomHeaderFooter
{
    public class CustomHeaderFooterPdfReport
    {
        readonly CustomHeader _customHeader = new CustomHeader();
        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\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight));
            })
            .PagesHeader(header =>
            {
                header.CustomHeader(_customHeader);
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.MultipleColumnsPerPage(new MultipleColumnsPerPage
                {
                    ColumnsGap = 22,
                    ColumnsPerPage = 2,
                    ColumnsWidth = 250,
                    IsRightToLeft = false,
                    TopMargin = 7
                });
            })
            .MainTableDataSource(dataSource =>
            {
                var rows = new List<Task>();
                var rnd = new Random();
                for (int i = 1; i < 210; i++)
                {
                    rows.Add(new Task
                    {
                        Assignee = new User
                        {
                            Id = i,
                            Name = "user-" + i
                        },
                        IsActive = rnd.Next(0, 2) == 1 ? true : false,
                        Name = "task-" + i
                    });
                }
                dataSource.StronglyTypedList(rows);
            })
            .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<Task>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(3);
                    column.HeaderCell("Task Name");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.Assignee.Name); // nested property support
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("Assignee");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.IsActive);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("Active");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.Checkmark(checkmarkFillColor: Color.Green, crossSignFillColor: Color.DarkRed);
                    });
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\CustomHeaderFooterPdfReportSample.pdf"));
        }
    }
}

به همراه Header سفارشی:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.CustomHeaderFooter
{
    public class CustomHeader : IPageHeader
    {
        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> rowdata, IList<SummaryCellData> summaryData)
        {
            return null;
        }

        Image _image;
        public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            if (_image == null) //cache is empty
            {
                var templatePath = AppPath.ApplicationPath + "\\data\\PdfHeaderTemplate.pdf";
                _image = PdfImageHelper.GetITextSharpImageFromPdfTemplate(pdfWriter, templatePath);
            }

            var table = new PdfPTable(1);
            var cell = new PdfPCell(_image, true) { Border = 0 };
            table.AddCell(cell);
            return table;
        }
    }
}

و Footer سفارشی استفاده شده:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Contracts;

namespace PdfReportSamples.CustomHeaderFooter
{
    public class CustomFooter : IPageFooter
    {
        PdfContentByte _pdfContentByte;
        readonly IPdfFont _pdfRptFont;
        readonly Font _font;
        readonly PdfRunDirection _direction;
        PdfTemplate _template;

        public CustomFooter(IPdfFont pdfRptFont, PdfRunDirection direction)
        {
            _direction = direction;
            _pdfRptFont = pdfRptFont;
            _font = _pdfRptFont.Fonts[0];
        }

        public void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData)
        {
            _template.BeginText();
            _template.SetFontAndSize(_pdfRptFont.Fonts[0].BaseFont, 8);
            _template.SetTextMatrix(0, 0);
            _template.ShowText((writer.PageNumber - 1).ToString());
            _template.EndText();
        }

        public void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData)
        {
            var pageSize = document.PageSize;
            var text = "Page " + writer.PageNumber + " / ";
            var textLen = _font.BaseFont.GetWidthPoint(text, _font.Size);
            var center = (pageSize.Left + pageSize.Right) / 2;
            var align = _direction == PdfRunDirection.RightToLeft ? Element.ALIGN_RIGHT : Element.ALIGN_LEFT;

            ColumnText.ShowTextAligned(
                        canvas: _pdfContentByte,
                        alignment: align,
                        phrase: new Phrase(text, _font),
                        x: center,
                        y: pageSize.GetBottom(25),
                        rotation: 0,
                        runDirection: (int)_direction,
                        arabicOptions: 0);

            var x = _direction == PdfRunDirection.RightToLeft ? center - textLen : center + textLen;
            _pdfContentByte.AddTemplate(_template, x, pageSize.GetBottom(25));
        }

        public void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData)
        {
            _pdfContentByte = writer.DirectContent;
            _template = _pdfContentByte.CreateTemplate(50, 50);
        }
    }
}

البته لازم به ذکر است که تمام این کدها به پوشه Samples سورس پروژه نیز جهت سهولت دسترسی، اضافه شده‌اند .

توضیحات:

برای پیاده سازی Header و Footer سفارشی در PdfReport نیاز خواهید داشت تا دو اینترفیس IPageHeader و IPageFooter را پیاده سازی کنید.
ساختار IPageHeader را در ذیل ملاحظه می‌کنید:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace PdfRpt.Core.Contracts
{
    public interface IPageHeader
    {
        PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData);

        PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData);
    }
}

RenderingGroupHeader مرتبط است به مباحث گروه بندی اطلاعات و گزارشات master-detail که در قسمت‌های بعد به آن‌ها اشاره خواهد شد. چون در اینجا به آن نیازی نداشتیم، تنها کافی است متد متناظر با آن، null بر گرداند که در کلاس CustomHeader فوق قابل مشاهده است.
متد RenderingReportHeader به ازای تولید هر صفحه جدید، فراخوانی خواهد شد. به عبارتی می‌توانید در صفحات مختلف، هدرهای مختلفی را نمایش دهید.
خروجی هر دو متد در اینجا یک جدول از نوع PdfPTable است. بنابراین هر نوع ساختار دلخواهی را که علاقمند هستید به شکل یک PdfPTable ایجاد کرده و بازگشت دهید. این جدول در هدر صفحات ظاهر خواهد شد.
برای نمونه در کلاس CustomHeader، یک قالب تهیه شده توسط Open Office توسط متد توکار PdfImageHelper.GetITextSharpImageFromPdfTemplate دریافت و تبدیل به تصویر می‌شود. این تصویر از نوع تصاویر قابل درک توسط iTextSharp است و نه اینکه واقعا تبدیل به یک تصویر معمولی مثلا از نوع bmp شود. سپس این تصویر، در یک ردیف از جدولی قرار داده شده و این جدول بازگشت داده می‌شود.
در کل یا توسط کار با PdfPTable می‌توانید یک هدر غیرپیش فرض را طراحی کنید و یا می‌توانید توسط ابزارهای بصری مانند Open Office یک قالب خاص را برای آن تهیه کرده و به روشی که ذکر شد و کدهای آن‌را ملاحظه می‌کنید، بارگذاری و استفاده کنید. این قالب‌ها در مسیر Bin\Data سورس‌های پروژه قرار داده شده‌اند.

ساختار IPageFooter به صورت زیر است:
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.Collections.Generic;

namespace PdfRpt.Core.Contracts
{
    public interface IPageFooter
    {
        void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData);

        void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData);

        void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData);
    }
}

برای طراحی یک Footer سفارشی کافی است اینترفیس فوق را پیاده سازی کنید که نمونه‌ای از آن‌را در کدهای کلاس CustomFooter ملاحظه می‌نمائید.
متد DocumentOpened، با وهله سازی شیء Document فراخوانی می‌شود.
متد PageFinished هر بار پیش از اتمام کار صفحه جاری و افزوده شدن آن به Document فراخوانی می‌گردد.
متد ClosingDocument، در زمان بسته شدن شیء Document فراخوانی خواهد شد.

اگر به امضای این متدها دقت کنید، شیء PdfWriter در اختیار شما قرار گرفته است که توسط آن می‌توان مستقیما بر روی فایل PDF، محتوایی را قرار داد. شیء Document نیز در دسترس است. مثلا توسط آن می‌توان اندازه دقیق صفحه را بدست آورد.
به علاوه پارامتر columnCellsSummaryData نیز امکان دسترسی به مقادیر ردیف‌های قبلی را در اختیار شما قرار می‌دهد. برای مثال اگر نیاز دارید تا بر اساس مقادیر ستون‌ها و ردیف‌های قبلی، محاسباتی را انجام داده و در پایین صفحات درج کنید، به این ترتیب دسترسی کاملی به آن‌ها، خواهید داشت.

استفاده از این کلاس‌های سفارشی نیز همواره به شکل زیر خواهد بود:
readonly CustomHeader _customHeader = new CustomHeader();
//...
.PagesFooter(footer =>
{
   footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight));
})
.PagesHeader(header =>
{
  header.CustomHeader(_customHeader);
})
کلا در PdfReport هر جایی متدی به نام CustomXYZ را مشاهده کردید، این متد یک اینترفیس را دریافت می‌کند. به عبارتی این امکان را خواهید داشت تا از متدهای پیش فرض مهیا صرفنظر کرده و مطابق نیاز، نسبت به پیاده سازی و استفاده از وهله جدیدی از این اینترفیس تعریف شده، اقدام کنید.
مطالب دوره‌ها
استفاده از async و await در برنامه‌های ASP.NET Web forms 4.5
سؤال: چه زمانی از متدهای async و چه زمانی از متدهای همزمان بهتر است استفاده شود؟

از متدهای همزمان متداول برای انجام امور ذیل استفاده نمائید:
- جهت پردازش اعمالی ساده و سریع
- اعمال مدنظر بیشتر قرار است بر روی CPU اجرا شوند و از مرزهای IO سیستم عبور نمی‌کنند.

و از متدهای غیرهمزمان برای پردازش موارد زیر کمک بگیرید:
- از وب سرویس‌هایی استفاده می‌کنید که متدهای نگارش async را نیز ارائه داده‌اند.
- عمل مدنظر network-bound و یا I/O-bound است بجای CPU-bound. یعنی از مرزهای IO سیستم عبور می‌کند.
- نیاز است چندین عملیات را به موازات هم اجرا کرد.
- نیاز است مکانیزمی را جهت لغو یک عملیات طولانی ارائه دهید.


مزایای استفاده از متدهای async در ASP.NET

استفاده از await در ASP.NET، ساختار ذاتی پروتکل HTTP را که اساسا یک synchronous protocol، تغییر نمی‌دهد. کلاینت، درخواستی را ارسال می‌کند و باید تا زمان آماده شدن نتیجه و بازگشت آن از طرف سرور، صبر کند. نحوه‌ی تهیه‌ی این نتیجه، خواه async باشد و یا حتی همزمان، از دید مصرف کننده کاملا مخفی است. اکنون سؤال اینجا است که چرا باید از متدهای async استفاده کرد؟
- پردازش موازی: می‌توان چند Task را مثلا توسط Task.WhenAll به صورت موازی با هم پردازش کرده و در نهایت نتیجه را سریعتر به مصرف کننده بازگشت داد. اما باید دقت داشت که این Taskها اگر I/O bound باشند، ارزش پردازش موازی را دارند و اگر compute bound باشند (اعمال محاسباتی)، صرفا یک سری ترد را ایجاد و مصرف کرده‌اید که می‌توانسته‌اند به سایر درخواست‌های رسیده پاسخ دهند.
- خالی کردن تردهای در حال انتظار: در اعمالی که disk I/O و یا network I/O دارند، پردازش موازی و اعمال async به شدت مقیاس پذیری سیستم را بالا می‌برند. به این ترتیب worker thread جاری (که تعداد آن‌ها محدود است)، سریعتر آزاد شده و به worker pool بازگشت داده می‌شود تا بتواند به یک درخواست دیگر رسیده سرویس دهد. در این حالت می‌توان با منابع کمتری، درخواست‌های بیشتری را پردازش کرد.


ایجاد Asynchronous HTTP Handlers در ASP.Net 4.5

در نگارش‌های پیش از دات نت 4.5، برای نوشتن فایل‌های ashx غیرهمزمان می‌بایستی اینترفیس IHttpAsynchHandler پیاده سازی می‌شد که نحوه‌ی کار با آن از مدل APM پیروی می‌کرد؛ نیاز به استفاده از یک سری callback داشت و این عملیات باید طی دو متد پردازش می‌شد. اما در دات نت 4.5 و با معرفی امکانات async و await، نگارش سازگاری با پیاده سازی کلاس پایه HttpTaskAsyncHandler فراهم شده است.
برای آزمایش آن، یک برنامه‌ی جدید ASP.NET Web forms نگارش 4.5 یا بالاتر را ایجاد کنید. سپس از منوی پروژه، گزینه‌ی Add new item یک Generic handler به نام LogRequestHandler.ashx را به پروژه اضافه نمائید.
زمانیکه این فایل به پروژه اضافه می‌شود، یک چنین امضایی را دارد:
 public class LogRequestHandler : IHttpHandler
IHttpHandler آن‌را اکنون به HttpTaskAsyncHandler تغییر دهید. سپس پیاده سازی ابتدایی آن به شکل زیر خواهد بود:
using System;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;

namespace Async14
{
    public class LogRequestHandler : HttpTaskAsyncHandler
    {
        public override async Task ProcessRequestAsync(HttpContext context)
        {
            string url = context.Request.QueryString["rssfeedURL"];
            if (string.IsNullOrWhiteSpace(url))
            {
                context.Response.Write("Rss feed URL is not provided");
            }

            using (var webClient = new WebClient {Encoding = Encoding.UTF8})
            {
                webClient.Headers.Add("User-Agent", "LogRequestHandler 1.0");
                var rssfeed = await webClient.DownloadStringTaskAsync(url);
                context.Response.Write(rssfeed);
            }
        }

        public override bool IsReusable
        {
            get { return true; }
        }

        public override void ProcessRequest(HttpContext context)
        {
            throw new Exception("The ProcessRequest method has no implementation.");
        }
    }
}
واژه‌ی کلیدی async را نیز جهت استفاده از await به نسخه‌ی غیرهمزمان آن اضافه کرده‌ایم.
در این مثال آدرس یک فید RSS از طریق کوئری استرینگ rssfeedURL دریافت شده و سپس محتوای آن به کمک متد DownloadStringTaskAsync دریافت و بازگشت داده می‌شود.
برای آزمایش آن، مسیر ذیل را درخواست دهید:
 http://localhost:4207/LogRequestHandler.ashx?rssfeedURL=https://www.dntips.ir/feed/latestchanges
کاربردهای فایل‌های ashx برای مثال ارائه فید‌های XML ایی یک سایت، ارائه منبع نمایش تصاویر پویا از بانک اطلاعاتی، ارائه JSON برای افزونه‌های auto complete جی‌کوئری و امثال آن است. مزیت آن‌ها سربار بسیار کم است؛ زیرا وارد چرخه‌ی طول عمر یک صفحه‌ی aspx معمولی نمی‌شوند.


صفحات async در ASP.NET 4.5

در قسمت‌های قبل مشاهده کردیم که در برنامه‌های دسکتاپ، به سادگی می‌توان امضای روال‌های رخداد گردان را به async تغییر داد و ... برنامه کار می‌کند. به علاوه از مزیت استفاده از واژه کلیدی await نیز در آن‌ها برخوردار خواهیم شد. اما ... هرچند این روش در وب فرم‌ها نیز صادق است (مثلا public void Page_Load را به  public async void Page_Load می‌توان تبدیل کرد) اما اعضای تیم ASP.NET آن‌را در مورد برنامه‌های وب فرم توصیه نمی‌کنند:
Async void event handlers تنها در مورد تعداد کمی از روال‌های رخدادگردان ASP.NET Web forms کار می‌کنند و از آن‌ها تنها برای تدارک پردازش‌های ساده می‌توان استفاده کرد. اگر کار در حال انجام اندکی پیچیدگی دارد، «باید» از PageAsyncTask استفاده نمائید. علت اینجا است که Async void یعنی fire and forget (کاری را شروع کرده و فراموشش کنید). این روش در برنامه‌های دسکتاپ کار می‌کند، زیرا این برنامه‌ها مدل طول عمر متفاوتی داشته و تا زمانیکه برنامه از طرف OS خاتمه نیابد، مشکلی نخواهند داشت. اما برنامه‌های بدون حالت وب متفاوتند. اگر عملیات async پس از خاتمه‌ی طول عمر صفحه پایان یابد، دیگر نمی‌توان اطلاعات صحیحی را به کاربر ارائه داد. بنابراین تا حد ممکن از تعاریف async void در برنامه‌های وب خودداری کنید.

تبدیل روال‌های رخدادگردان متداول وب فرم‌ها به نسخه‌ی async شامل دو مرحله است:
الف) از متد جدید RegisterAsyncTask که در کلاس پایه Page قرار دارد برای تعریف یک PageAsyncTask استفاده کنید:
using System;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.UI;

namespace Async14
{
    public partial class _default : Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            RegisterAsyncTask(new PageAsyncTask(LoadSomeData));
        }

        public async Task LoadSomeData()
        {
            using (var webClient = new WebClient { Encoding = Encoding.UTF8 })
            {
                webClient.Headers.Add("User-Agent", "LogRequest 1.0");
                var rssfeed = await webClient.DownloadStringTaskAsync("url");

                //listcontacts.DataSource = rssfeed;
            }
        }
    }
}
با استفاده از System.Web.UI.PageAsyncTask می‌توان یک async Task را در روال‌های رخدادگردان ASP.NET مورد استفاده قرار داد.

ب) سپس در کدهای فایل aspx، نیاز است خاصیت async را نیز true نمائید:
 <%@ Page Language="C#" AutoEventWireup="true"
Async="true"
  CodeBehind="default.aspx.cs" Inherits="Async14._default" %>


تغییر تنظیمات IIS برای بهره بردن از پردازش‌های Async

اگر از ویندوزهای 7، ویستا و یا 8 استفاده می‌کنید، IIS آن‌ها به صورت پیش فرض به 10 درخواست همزمان محدود است.
بنابراین تنظیمات ذیل مرتبط است به یک ویندوز سرور و نه یک work station :
به IIS manager مراجعه کنید. سپس برگه‌ی Application Pools آن‌را باز کرده و بر روی Application pool برنامه خود کلیک راست نمائید. در اینجا گزینه‌ی Advanced Settings را انتخاب کنید. در آن Queue Length را به مثلا عدد 5000 تغییر دهید. همچنین در دات نت 4.5 عدد 5000 برای MaxConcurrentRequestsPerCPU نیز مناسب است. به علاوه عدد connectionManagement/maxconnection را نیز به 12 برابر تعداد هسته‌های موجود تغییر دهید.
مطالب
با HttpHandler بیشتر آشنا شوید
در  مقاله قبل توضیح دادیم که وظیفه httphandler رندر و پردازش خروجی یک درخواست هست؛ حالا در این مقاله قصد داریم که مفهوم httphandler را بیشتر بررسی کنیم.

HttpHandler
برای تهیه‌ی یک httphandler، باید کلاسی را بر اساس اینترفیس IHttpHandler پیاده سازی کنیم و بعدا آن را در web.config برنامه معرفی کنیم. برای پیاده سازی این اینترفیس، به یک متد به اسم ProcessRequest با یک پارامتر از نوع HttpContext و یک پراپرتی به اسم IsReusable نیاز داریم که مقدار برگشتی این پراپرتی را false بگذارید؛ بعدا خواهم گفت چرا اینکار را می‌کنیم. نحوه‌ی پیاده‌سازی یک httphandler به شکل زیر است:
public class MyHttpHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {        
    }

    public bool IsReusable
    {
        get { return false; }
    }
}
با استفاده از شیء context می‌توان به دو شیء httpresponse و httprequest دسترسی داشت. تکه کد زیر مثالی است در مورد نحوه‌ی تغییر در محتوای سایت:
public class MyHttpHandler : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        HttpResponse response = context.Response;
        HttpRequest request = context.Request;

        response.Write("Every Page has a some text like this");
    }

    public bool IsReusable
    {
        get { return false; }
    }
}
بگذارید همین کد ساده را در وب کانفیگ معرفی کنیم:
<system.web>
  <httpHandlers>
      <add verb="*" path="*.aspx" type="MyHttpHandler"/>
  </httpHandlers>
</system.web>
اگر نسخه IIS شما همانند نسخه‌ی من باشد، نباید هیچ تغییری مشاهده کنید؛ زیرا کد بالا فقط در مورد نسخه‌ی IIS6 صدق می‌کند و برای نسخه‌های IIS 7 به بعد باید به شیوه زیر عمل کنید:
<configuration>
  <system.web>
    <httpHandlers>

  <add name="myhttphandler" verb="*" path="*.aspx" type="MyHttpHandler"/>

    </httpHandlers>
  </system.web>
</configuration>
خروجی نهایی باید تنها این متن باشد: Every Page has a some text like this 
گزینه Type که نام کلاس می‌باشد و اگر کلاس داخل یک فضای نام قرار گرفته باشد، باید اینطور نوشت : namespace.ClassName  
گزینه verb شامل مقادیری چون Get,Post,Head,Putو Delete می‌باشد و httphandler را فقط برای این نوع درخواست‌ها اجرا می‌کند و در صورتیکه بخواهید چندتا از آن‌ها را استفاده کنید، با , از هم جدا می‌شوند. مثلا Get,post و درصورتیکه همه‌ی گزینه‌ها را بخواهید علامت * را میتوان استفاده کرد. 
 گزینه‌ی path این امکان را به شما می‌دهد که مسیر و نوع فایل‌هایی را که قصد دارید روی آن‌ها فقط اجرا شود، مشخص کنید و ما در قطعه کد بالا گفته‌ایم که تنها روی فایل‌هایی با پسوند aspx اجرا شود و چون مسیری هم ذکر نکردیم برای همه‌ی مسیرها قابل اجراست. یکی از مزیت‌های دادن پسوند این است که می‌توانید پسوندهای اختصاصی داشته باشید. مثلا پسوند RSS برای فیدهای وب سایتتان. بسیاری از برنامه نویسان به جای استفاده از صفحات aspx از ashx استفاده می‌کنند که به مراتب سبک‌تر از aspx هست و شامل بخش ui نمی‌شود و نتیجه خروجی آن بر اساس کدی که می‌نویسید مشخص می‌شود که میتواند صفحه متنی یا عکس یا xml یا ... باشد. در اینجا در مورد ساخت صفحات ashx توضیح داده شده است. 

  IHttpHandlerFactory
کار این اینترفیس پیاده سازی یک کلاس است که خروجی آن یک کلاس از نوع IHttpHandler هست. اگر دقت کنید در مثال‌های قبلی ما برای معرفی یک هندلر در وب کانفیگ یک سری path را به آن میدادیم و برای نمونه aspx.* را معرفی می‌کردیم؛ یعنی این هندلر را بر روی همه‌ی فایل‌های aspx اجرا کن و اگر دو یا چند هندلر در وب کانفیگ معرفی کنیم و برای همه مسیر aspx را قرار بدهیم، یعنی همه این هندلرها باید روی صفحات aspx اجرا گردند ولی در httphandlerfactory، ما چند هندلر داریم و میخواهیم فقط یکی از آن‌ها بر روی صفحات aspx انجام بگیرد، پس ما یک هندلرفکتوری را برای صفحات aspx معرفی می‌کنیم و در حین اجرا تصمیم می‌گیریم که کدام هندلر را ارسال کنیم.
اجازه بدهید نوشتن این نوع کلاس را آغاز کنیم،ابتدا دو هندلر به نام‌های httphandler1 و httphandler2 می‌نویسیم :
public class MyHttpHandler1 :IHttpHandler
{
   
    public void ProcessRequest(HttpContext context)
    {
        HttpResponse response = context.Response;
        response.Write("this is httphandler1");
    }

    public bool IsReusable
    {
        get { return false; }
    }
}

public class MyHttpHandler2 : IHttpHandler
{

    public void ProcessRequest(HttpContext context)
    {
        HttpResponse response = context.Response;
        response.Write("this is httphandler2");
    }

    public bool IsReusable
    {
        get { return false; }
    }
}
سپس کلاس MyFactory را بر اساس اینترفیس IHttpFactory پیاده سازی می‌کنیم و باید دو متد برای آن صدا بزنیم؛ یکی که هندلر انتخابی را بر میگرداند و دیگری هم برای رها کردن یا آزادسازی یک هندلر هست که در این مقاله کاری با آن نداریم. عموما GC دات نت در این زمینه کارآیی خوبی دارد. در قسمت هندلرهای غیرهمزمان به طور مختصر خواهیم گفت که GC چطور آن‌ها را مدیریت می‌کند. کد زیر نمونه کلاسی است که توسط IHttpFactory پیاده سازی شده است:
public class MyFactory : IHttpHandlerFactory
{
    public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated)
    {
        
    }

    public void ReleaseHandler(IHttpHandler handler)
    {
        
    }
}
در متد GetHandler چهار آرگومان وجود دارند که به ترتیب برای موارد زیر به کار می‌روند:
 Context یک شی از کلاس httpcontext که دسترسی ما را برای اشیاء سروری چون response,request,session و... فراهم می‌کند.
 RequestType  مشخص می‌کند که درخواست صفحه به چه صورتی است. این گزینه برای مواردی است که verb بیش از یک مورد را حمایت می‌کند. برای مثال دوست دارید یک هندلر را برای درخواست‌های Get ارسال کنید و هندلر دیگر را برای درخواست‌های نوع Post
 URL مسیر مجازی virtual Path صفحه صدا زده شده 
 PathTranslated مسیر فیزیکی صفحه درخواست کننده را ارسال می‌کند. 
متد GetHandler را بدین شکل می‌نویسیم و میخواهیم همه صفحات aspx هندلر شماره یک را انتخاب کنند و صفحات aspx که نامشان با t شروع می‌شوند، هندلر  شماره دو را انتخاب کند:
 public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated)
    {
        string handlername = "MyHttpHandler1";
        if(url.Substring(url.LastIndexOf("/")+1).StartsWith("t"))
        {
            handlername = "MyHttpHandler2";
        }

        try
        {
            return (IHttpHandler) Activator.CreateInstance(Type.GetType(handlername));
        }
        catch (Exception e) 
        {
            throw new HttpException("Error: " + handlername, e);
        }
    }

    public void ReleaseHandler(IHttpHandler handler)
    {
        
    }
}
شی Activator که برای ساخت اشیاء با انتخاب بهترین constructor موجود بر اساس یک نوع Type مشخص به کار می‌رود و خروجی Object را می‌گرداند؛ با یک تبدیل ساده، خروجی به قالب اصلی خود باز میگردد. برای مطالعه بیشتر در مورد این کلاس به اینجا و اینجا مراجعه کنید.
نحوه‌ی تعریف factory در وب کانفیگ مانند قبل است و فقط باید در Type به جای نام هندلر نام فکتوری را نوشت. برنامه را اجرا کنید تا نتیجه آن را ببینیم:
تصویر زیر نتیجه صدا زده شدن فایل default.aspx است:

تصویر زیر نتیجه صدا زده شدن فایل Tours_List.aspx است:

AsyncHttpHandlers
برای اینکه کار این اینترفیس را درک کنید بهتر هست اینجا را مطالعه کنید. در اینجا به خوبی تفاوت متدهای همزمان و غیرهمزمان توضیح داده شده است.
متن زیر خلاصه‌ترین و بهترین توضیح برای این پرسش است، چرا غیرهمزمان؟
در اعمالی که disk I/O و یا network I/O دارند، پردازش موازی و اعمال async به شدت مقیاس پذیری سیستم را بالا می‌برند. به این ترتیب worker thread جاری (که تعداد آن‌ها محدود است)، سریعتر آزاد شده و به worker pool بازگشت داده می‌شود تا بتواند به یک درخواست دیگر رسیده سرویس دهد. در این حالت می‌توان با منابع کمتری، درخواست‌های بیشتری را پردازش کرد. 
موقعی که اینترفیس IHttpAsyncHandler را ارث بری کنید (این اینترفیس نیز از IHttpHandler ارث بری کرده است و دو متد اضافه‌تر دارد)، باید دو متد دیگر را نیز پیاده سازی کنید:
 public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj)
    {
        
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        
    }
پراپرتی ISResuable هم موقعی که true برگشت بدهد، باعث می‌شود pooling فعال شده و این هندلر در حافظه باقی بماند و تمامی درخواست‌ها از طریق همین یک نمونه اجرا شوند.
به زبان ساده‌تر، این پراپرتی می‌گوید اگر چندین درخواست از طرف کلاینت‌ها برسد، توسط یک نمونه یا instance از هندلر پردازش خواهند شد؛ چون به طور پیش فرض موقعی که تمام درخواست‌های از pipeline بگذرند، هندلر‌ها توسط httpapplication در یک لیست بازیافت قرار گرفته و همه‌ی آن‌ها با null مقداردهی می‌شوند تا از حافظه پاک شوند ولی اگر این پراپرتی true برگرداند، هندلر مربوطه نال نشده و برای پاسخگویی به درخواست‌های بعدی در حافظه خواهد ماند.
مهمترین مزیت این گزینه، این می‌باشد که کاآیی سیستم را بالا می‌برد و اشیا کمتری به GC پاس می‌شوند. ولی یک عیب هم دارد که این تردهایی که ایجاد می‌کند، امنیت کمتری دارند و باید توسط برنامه نویس این امنیت بالاتر رود. این پراپرتی را در مواقعی که با هندلرهای همزمان کار می‌کنید برابر با false بگذارید چون این گزینه بیشتر بر روی هندلرهای غیرهمزمان اثر دارد و هم اینکه بعضی‌ها توصیه می‌کنند که false بگذارید چون GC مدیریت خوبی در مورد هندلرها دارد و هم این که ارزش یافتن باگ در کد را ندارد.
بر میگردیم سراغ کد نویسی هندلر غیر همزمان. در آخرین قطعه کد نوشته شده، ما دو متد دیگر را پیاده سازی کردیم که یکی از آن‌ها BeginProcessRequest  است و خروجی آن کلاسی است که از اینترفیس IAsyncResult  ارث بری کرده است. پس یک کلاس با ارث بری از این اینترفیس می‌نویسیم و در این کلاس نیاز است که 4 پراپرتی را پیاده سازی کنیم که این کلاس به شکل زیر در خواهد آمد:
public class AsynchOperation : IAsyncResult
{
    private bool _completed;
    private Object _state;
    private AsyncCallback _callback;
    private HttpContext _context;

    bool IAsyncResult.IsCompleted { get { return _completed; } }
    WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } }
    Object IAsyncResult.AsyncState { get { return _state; } }
    bool IAsyncResult.CompletedSynchronously { get { return false; } }
}
متدهای private اجباری نیستند؛ ولی برای ذخیره مقادیر get و set نیاز است. همانطور که از اسامی آن‌ها پیداست مشخص است که برای چه کاری ساخته شده اند.
خب اجازه بدهید یک تابع سازنده به آن برای مقداردهی اولیه این متغیرهای خصوصی داشته باشیم:
   public AsynchOperation(AsyncCallback callback, HttpContext context, Object state)
    {
        _callback = callback;
        _context = context;
        _state = state;
        _completed = false;
    }
همانطور که می‌بینید موارد موجود در متد BeginProcessRequest را تحویل می‌گیریم تا اطلاعات درخواستی مربوطه را داشته باشیم و مقدار _Completed را هم برابر با false قرار می‌دهیم. سپس نوبت این می‌رسد که ما درخواست را در صف pool قرار دهیم. برای همین تکه کد زیر را اضافه می‌کنیم: 
   public void StartAsyncWork()
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null);
    }
با اضافه شدن درخواست به صف، هر موقع درخواست‌های قبلی تمام شوند و callback خودشان را ارسال کنند، نوبت درخواست‌های جدیدتر هم میرسد. StartAsyncTask هم متدی است که وظیفه‌ی اصلی پردازش درخواست را به دوش دارد و موقعی که نوبت درخواست برسد، کدهای این متد اجرا می‌گردد که ما در اینجا مانند مثال اول روی صفحه چیزی نوشتیم:
 private void StartAsyncTask(Object workItemState)
    {

        _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n");

        _context.Response.Write("Hello World from Async Handler!");
        _completed = true;
        _callback(this);
    }
دو خط اول اطلاعات را چاپ کرده و در خط سوم متغیر _completed را true کرده و در آخر این درخواست را فراخوانی مجدد می‌کنیم تا بگوییم که کار این درخواست پایان یافته‌است؛ پس این درخواست را از صف بیرون بکش و درخواست بعدی را اجرا کن.
نهایتا کل این کلاس را در متد BeginProcessRequest  صدا بزنید:
context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n");
        AsynchOperation asynch = new AsynchOperation(callback, context, obj);
        asynch.StartAsyncWork();
        return asynch;
کل کد مربوطه : (توجه:کدها از داخل سایت msdn برداشته شده است و اکثر کدهای موجود در نت هم به همین قالب می‌نویسند)
public class MyHttpHandler : IHttpAsyncHandler
{
    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj)
    {
        context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n");
        AsynchOperation asynch = new AsynchOperation(callback, context, obj);
        asynch.StartAsyncWork();
        return asynch;
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        
    }
    public void ProcessRequest(HttpContext context)
    {
       throw new InvalidOperationException(); 

    }

    public bool IsReusable
    {
        get { return false; }
    }
}

public class AsynchOperation : IAsyncResult
{
    private bool _completed;
    private Object _state;
    private AsyncCallback _callback;
    private HttpContext _context;

    bool IAsyncResult.IsCompleted { get { return _completed; } }
    WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } }
    Object IAsyncResult.AsyncState { get { return _state; } }
    bool IAsyncResult.CompletedSynchronously { get { return false; } }

    public AsynchOperation(AsyncCallback callback, HttpContext context, Object state)
    {
        _callback = callback;
        _context = context;
        _state = state;
        _completed = false;
    }


    public void StartAsyncWork()
    {
        
        ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null);

    }
    private void StartAsyncTask(Object workItemState)
    {

        _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n");

        _context.Response.Write("Hello World from Async Handler!");
        _completed = true;
        _callback(this);
    }

آشنایی با فایل ASHX
در مطالب بالاتر به فایل‌های Ashx اشاره کردیم. این فایل به نام Generic Web Handler شناخته می‌شوند و می‌توانید با Add New Item این نوع فایل‌ها را اضافه کنید. این فایل شامل هیچ UI ایی نمی‌باشد و فقط شامل بخش کد می‌باشد. برای همین نسبت به aspx سبک‌تر بوده و شامل یک directive به اسم  WebHandler@ است.
مایکروسافت در MSDN نوشته است که httphandler‌ها در واقع فرآیندهایی هستند (به این فرایندها بیشتر End Point می‌گویند) که در پاسخ به درخواست‌های رسیده شده توسط asp.net application اجرا می‌شوند و بیشترین درخواست هایی هم که می‌رسد از نوع صفحات Aspx می‌باشد و موقعی که کاربری درخواست صفحه‌ی aspx می‌کند هندلرهای مربوط به page اجرا می‌شوند.
در متن بالا به خوبی روشن هست که ashx به دلیل نداشتن UI، تعداد کمتری از handlerها را در مسیر Pipeline قرار می‌دهند و اجرای آن‌ها سریعتر است. غیر از این دو هندلر aspx و ashx، هندلر توکار دیگری چون asmx که مختص وب سرویس هست و axd مربوط به اعمال trace نیز وجود دارند.

در این لینک که در بالاتر هم درج شده بود یک نمونه هندلر برای نمایش تصویر نوشته است. اگر تصاویرتان را بدین صورت اجرا کنید می‌توان جلوی درخواست‌های رسیده از وب سایت‌های دیگر را سد کرد. برای مثال یک نفر مطالب شما را کپی می‌کند و در داخل وبلاگ یا وب سایتش می‌گذارد و شما در اینجا درخواست‌های رسیده خارج از وب سایت خود را لغو خواهید کرد و تصاویر کپی شده نمایش داده نخواهند شد.
مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت پانزدهم
در این قسمت قصد داریم تا با زدن کدهای Platform Specific در Xamarin آشنا شویم. صد البته که در Xamarin Forms به کتابخانه‌های NET. ای دسترسی داریم و مواردی چون Entity Framework Core، Auto Mapper، Autofac و ... را می‌توانیم استفاده کنیم و در کنار اینها، مواردی چون Linq, Parallel Linq, Socket و ... نیز در دسترس ما هستند. در رابطه با مواردی چون کار با Clipboard, Geocoding, Gyroscope, Secure Store و ... نیز می‌توان از کتابخانه فوق العاده کاربردی Xamarin Essentials استفاده کرد که با یک کد CSharp می‌توانید روی Android/iOS/Windows جواب بگیرید.
اما فرض کنید که جستجو کرده اید و کد Cross Platform آماده‌ای برای استفاده نیافته‌اید؛ یا پیدا کرده‌اید، ولی صد در صد منطبق با نیازهای شما نیست. حال باید چه کنید؟ ابتدا باید کد مربوطه را بدانید که در Android/iOS/Windows (بسته به نیازتان) چگونه باید نوشت. در مورد Windows، خب تمامی امکانات سیستم عامل ویندوز را در زبان CSharp هم دارید. خبر خوب این است که این مهم نه تنها برای ویندوز که در مورد Android و iOS نیز برقرار است. به علاوه مستندات استفاده از آنها به زبان CSharp نیز موجود است. برای مثال نگاهی بیاندازید به روش Platform Specific استفاده از Bluetooth در Windows و AR Kit 2 در iOS و Job Scheduler در Android
صد البته که کتابخانه فوق العاده BluetoothLE وجود دارد و یک بار نوشتن کد، نه تنها روی Windows/Android/iOS که بر روی macOS و tvOS هم کار می‌کند!

با مثال گرفتن "ورژن برنامه" شروع می‌کنیم. هر چند با استفاده از Xamarin Essentials می شود با یک خط کد، ورژن برنامه را در هر پلتفرمی که باشیم گرفت؛ ولی فرض کنید که نمی‌شود. برای پیاده سازی این قابلیت ابتدا یک Interface را تعریف می‌کنیم و آن را در فولدر Contracts در پروژه XamApp قرار می‌دهیم:
public interface IAppVersionService
{
    string GetAppVersion();
}
سپس در پروژه XamApp.Android، در فولدر Implementations، کلاس زیر را می‌سازیم: (چون این کلاس در پروژه Android است، به 100% امکانات Android دسترسی داریم)
public class AndroidAppVersionService : IAppVersionService
{
    public Android.Content.Context Context { get; set; }

    public string GetAppVersion()
    {
        return Context.PackageManager.GetPackageInfo(Context.PackageName, 0).VersionName;
    }
}
این کد را از روی این جواب در StackOverFlow نوشته‌ام. همانطور که می‌بینید، دو کد، ساختاری شبیه به یکدیگر دارند. فقط تفاوت این است که Context.GetPackageManager در Java، در CSharp به Context.PackageManager تبدیل می‌شود؛ زیرا در Java چیزی به صورت Property و Get,Set وجود ندارد و Context.PackageManager در Java معادل می‌شود با دو متد Context.GetPackageManager و Context.SetPackageManager
تقریبا برای هر کاری در Android نیاز به Context دارید که می‌توانید آن را با Property Injection دریافت کنید.
سپس در فایل MainActivity.cs در کلاس XamAppPlatformInitializer، در متد RegisterTypes داریم:
containerBuilder.RegisterType<AndroidAppVersionService>()
    .As<IAppVersionService>()
    .PropertiesAutowired(PropertyWiringOptions.PreserveSetValues);
برای پیاده سازی همین امکان در iOS داریم:
public class iOSAppVersionService : IAppVersionService
{
    public string GetAppVersion()
    {
        var infoDictionary = NSBundle.MainBundle.InfoDictionary;
        return infoDictionary?["CFBundleShortVersionString"] as NSString;
    }
}
که از روی این جواب به دست آمده است. البته جواب مربوطه علاوه بر ورژن، نام برنامه را نیز به دست می‌آورد که نیاز ما نیست. اگر سایر جواب‌ها را نگاه کنید، می‌بینید که جواب‌های مربوط به Swift برای برنامه نویسان CSharp خوانایی دارند، ولی این در مورد کدهای Objective-C خیلی صادق نیست(!) برای حل این مشکل، کد Objective-C را در این سایت به Swift تبدیل کرده و سپس معادل CSharp آن را بنویسید.
و در نهایت برای UWP از روی این جواب داریم:
public string GetAppVersion()
{
    return $"{Package.Current.Id.Version.Major}.{Package.Current.Id.Version.Minor}"; 
}
که این دو نیز در AppDelegate.cs برای iOS و MainPage.xaml.cs برای UWP رجیستر می‌شوند.
برای استفاده نیز کافی است در هر View Model ای که قصد استفاده از این سرویس را دارید، یک Property از جنس IAppVersionService را تعریف کنید. در صورت Pull کردن آخرین تغییرات پروژه XamApp، می‌توانید نتیجه را در View و View Model با نام PlatformSpecificSamples ببینید.
خبر خوب این است که تمامی کدها به زبان CSharp نوشته می‌شوند و اگر مثلا وسط یک کد Platform Specific برای Android احتیاج به Auto Mapper پیدا کردید، می‌توانید از آن استفاده کنید. همچنین تمامی این کدها در Visual Studio دیباگ می‌شوند که خود نعمتی است.
حال اگر در ادامه کار، به یک کتابخانه 3rd Party که با Java نوشته شده نیاز پیدا کردیم چه؟ برای مثال این کتابخانه اطلاعاتی را در مورد Ringer گوشی، در اختیار ما قرار می‌دهد!
در Xamarin می‌توانید فایل‌های JAR و AAR و Header‌های Objective-C و Swift را در پروژه اضافه کنید و Wrapper به زبان CSharp تحویل بگیرید! علاوه بر مستندات مفصل خود Xamarin در این مورد که برای Android/iOS می توانید آنها را بخوانید. افراد زیادی بر همین اساس امکان استفاده از کتابخانه‌های 3rd Party زیادی را به Xamarin اضافه کرده‌اند. برخی از ابزارها نیز در این زمینه کاربردی هستند؛ برای مثال، برای ساخت C# Wrapper از روی C++,C از ابزار CppSharp می توانید استفاده کنید.
در نظر داشته باشید، اگر بخواهید کدی بزنید که فقط تفاوت رفتار در Android/iOS/Windows را دارد، یا بسته به گوشی، تبلت یا دسکتاپ بودن قرار است رفتارش تفاوت کند، مثلا یک پیام را فقط به دارندگان گوشی‌های اندرویدی نشان دهید، ولی با IUserDialogs که در هر سه پلتفرم کار می‌کند می‌خواهید این کد را بنویسید، احتیاجی به این کارها نیست و به سادگی تعریف یک Property با نام IDeviceService می‌توانید جواب لازم را بگیرید:
async Task ShowSomeAlertToAndroidPhoneUsersOnly()
{
    if (DeviceService.RuntimePlatform == RuntimePlatform.Android && DeviceService.Idiom == TargetIdiom.Phone)
    {
        await UserDialogs.AlertAsync("Some alert to android phone users only!", "Test");
    }
}

در برخی مواقع ما قصد سفارشی سازی کردن کنترل‌های UI را داریم. برای مثال زمانیکه از Entry در Xamarin Forms استفاده می‌کنیم، این به کنترل معادل Native خودش در هر پلتفرم تبدیل می‌شود، که همین باعث می‌شود بگوییم UI در Xamarin Forms به صورت Native است. حال در iOS که ما UITextField را به عنوان معادل Native کنترل Entry داریم، یک ویژگی داریم به نام ClearButtonMode که وقتی به مقدار WhileEditing تنظیم شود، در موقع تایپ کردن در UITextField، آن X پاک کردن متن باقی می‌ماند. این رفتار پیش فرض نیست و اگر ما قصد تغییر آن را داشته باشیم، یکی از متداول‌ترین راه‌ها، نوشتن Custom Renderer است. برای همین در iOS از EntryRenderer ارث بری می‌کنیم و سفارشی سازی مربوطه را انجام می‌دهیم و در نهایت EntryRenderer خودمان را رجیستر می‌کنیم.
public class XamAppEntryRenderer : EntryRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
    {
        base.OnElementChanged(e);

        if (e.NewElement != null) /* e.NewElement is a Xamarin Forms' Entry */
        {
            Control.ClearButtonMode = UITextFieldViewMode.WhileEditing; // Control is UITextField
        }
    }
}
برای Register کردن نیز داریم:
[assembly: ExportRenderer(typeof(Entry), typeof(XamAppEntryRenderer))]
در واقع این کد می‌گوید که از این به بعد، Entry‌ها در iOS، با کلاس جدید Render شوند. برای درک بهتر این مهم، فایل XamAppEntryRenderer.cs را در فولدر Renderer در پروژه XamApp.iOS مشاهده کنید.
مطالب
آشنایی اولیه با gRPC
در مقاله‌ی قبلی بطور کلی با Protocol ‌Buffers آشنا شدیم. در این قسمت با gRPC  آشنا شده و همچنین به پیاده سازی یک سرور و کلاینت، با استفاده از gRPC پرداخته که توسط آن به تبادل اطلاعات با یکدیگر میپردازند. 
gRPC یک فریم ورک مدرن و متن باز با کارآیی بالاست. توسط گوگل پیاده سازی شده و جزء انجمن CNCF میباشد (مثل Docker & Kubernetes) که بر روی سیستم عامل‌های متعددی اجرا میشود. به صورت خیلی کارا میتواند سرویس‌های متعددی را به یکدیگر متصل کرده و همچنین از امکاناتی همچون load balancing, monitoring, tracing, health checking, authentication به صورت خیلی ساده پشتیبانی میکند. بسایر سریع و همچنین Low Latency است. مستقل از یک زبان برنامه نویسی خاص هست و برای streaming بسیار مناسب است و همچنین برای سیستم‌های توزیع شده پیشنهاد میشود و به راحتی قابل توسعه و نگهداری است.
راجع به مزایای gRPC بسیار صحبت کردیم. برای طراحی سرویس‌های متعددی که با یکدیگر در ارتباط هستند، مناسب میباشد. از HTTP/2 به صورت پیشفرض استفاده میکند (راجع به تفاوت HTTP/2 و HTTP1 اینجا  را مطالعه بفرمایید).
شاید بزرگترین مشکلی که در حال حاضر دارد این است که REST را پشتیبانی نمیکند. بدین معنا که شما از طریق browser نمیتوانید یک در خواست را به یک سرور پیاده سازی شده توسط gRPC بصورت مستقیم ارسال کنید. راه حل اول برای حل این مشکل، پیاده سازی یک restful gateway با ابزار دلخواه خود و بقیه سرویس‌ها بعد از آن به هم از طریق gRPC ارتباط برقرا میکنند، یا راه حل بهتر اینکه از grpc-gateway  استفاده شود. ابزاری است که به کمک آن میتوانید سیستم خود را با REST یکپارچه سازی نمایید (هر چند راه‌های دیگری برای وصل شدن از مرورگر به یک سرور gRPC با استفاده از کتابخانه‌های third party میسر شده، اما خارج از موضوع بحث است و مطالعه‌ی بیشتر را به خواننده واگذار میکنم)
قدم اول در پیاده سازی یک سرور/کلاینت با استفاده از gRPC، آشنایی با protocol buffers هست. برای آشنایی، به مقاله‌ی قبلی رجوع فرمایید. تمامی پیاده سازی‌های ما از روی کد‌های تولید شده از تعاریف protocol bufferهایی هست که نوشته‌ایم.
حال فرض کنید میخواهیم یک سرور gRPC را با استفاده از #C نوشته و پیاده سازی نماییم:
۱) قدم اول قطعا نوشتن protobuf می‌باش‍د‍‍. همانطور که در مقاله‌ی قبلی ذکر شده است، به صورت زیر، مدل و همچنین متد‌های لازم را معرفی مینماییم و نام آن را helloworld.proto قرار میدهیم.
syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
مدل و سرویس‌ها بصورت واضحی نوشته شده‌اند؛ SayHello با ورودی HelloRequest و خروجی HelloReply تعیین شده‌است.
۲) حالا کافی است یک پروژه‌ی Console را ساخته و ابتدا پکیج‌های زیر را نصب نماییم.
Google.Protobuf
grpc
Grpc.Tools
از طریق Grpc.Tools میتوانیم protobuf‌های خود را بصورت خودکار بعد از build تولید نماییم. در csproj آیتم زیر را اضافه کرده و آدرس protobuf را تعیین مینماییم.
<ItemGroup>
      <Protobuf Include="helloworld.proto" />
  </ItemGroup>
حال کافی است کد‌های زیر را جایگزین نماییم:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Helloworld;
using Grpc.Core;

namespace ServerGrpc
{
    class GreeterImpl : Greeter.GreeterBase
    {
        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            System.Console.WriteLine("request made!");
            return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
        }
    }

    class Program
    {
       const int Port = 50051;

        public static void Main(string[] args)
        {
            Server server = new Server
            {
                Services = { Greeter.BindService(new GreeterImpl()) },
                Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) }
            };
            server.Start();

            Console.WriteLine("Greeter server listening on port " + Port);
            Console.WriteLine("Press any key to stop the server...");
            Console.ReadKey();

            server.ShutdownAsync().Wait();
        }
    }
}
همانطور که مشاهده میکنید، مدل‌ها و سریس‌ها بصورت خودکار تولید شده‌اند (ضمن اینکه میتوانستیم بصورت دستی نیز protobuf را برای زبان دلخواه خود تولید نماییم).
سرور را بر روی پورت مشخصی ایجاد کرده و همچنین سرویس مورد نظرمان را پیاده سازی کرده‌ایم؛ به صورت فوق همه چیز به ساده‌ترین صورت در نظر گرفته شده است.
gRPC به صورت خودکار از پروتکل امن ssl استفاده میکند؛ اما برای راحتی کار ما از آن استفاده نکرده‌ایم.
نکته: فایل‌های generate شده را از طریق آدرس زیر میتوانید پیدا کنید:
obj/Debug/netcoreapp2.2(یا نسخه‌ی دیگری که استفاده میکنید)

حالا بنا داریم یک کلاینت را با یک زبان برنامه نویسی کاملا مجزا نوشته و به سرور grpc متصل شویم. این کلاینت را با زبان Go خواهیم نوشت (بدیهی است می‌توان جای زبان‌های برنامه نویسی کلاینت و سرور را تغییر داد).
نکته: خیلی وارد جزیات زبان Go نمی‌شویم و فقط اشاره‌ای به موارد کلی خواهیم کرد.
ابتدا باید از روی protobuf کد مربوط به Go را تولید نماییم؛ به صورت زیر:
protoc helloworld.proto --go_out=plugins=grpc:.
فرض کنید فایل generate شده در پوشه‌ی proto قرار گرفته به نام "helloworld.pb.go"
یک فایل به نام main.go ساخته و کد‌های زیر را وارد مینماییم.
package main
import (
        "fmt"
        "golang.org/x/net/context"
        "google.golang.org/grpc"
        "gosample/proto"
)
func main() {
    initial()
}

func initial(){
    conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
    defer conn.Close()
    client := helloworld.NewGreeterClient(conn)
    data, _ := client.SayHello(context.Background(), &helloworld.HelloRequest{Name : "Ali"})

    fmt.Println(data)
}

 به سرور به صورت insecure متصل شده ایم؛ آخر برنامه connection را می‌بندیم و SayHello را فراخوانی کرده و جواب را بر روی خروجی نمایش میدهیم.
نکته: gosample اسم پروژه‌ای است که من ساخته‌ام و proto آدرس پوشه‌ای است که فایل تولید شده‌ی grpc داخل آن قرار گرفته‌است؛ بقیه نیز کتابخانه‌های لازم برای کار با grpc میباشد.
نکته: gRPC برای streaming دیتا بسیار مناسب است (هم یکطرفه و همینطور دو طرفه).
نکته: به دلیل سادگی کار با ابزار‌های مختلف، انتخاب خیلی خوبی برای سیستم‌های توزیع شده‌است؛ همانطور که مشاهده کردید به راحتی قابلیت تعامل بین زبان‌های برنامه نویسی متعددی برقرار است.
نکته‌ی آخر: از وارد شدن به موارد ریز اجتناب کرده‌ام و صرفا این مقاله جهت آشنایی و دید کلی نسبت به این موضوع در نظر گرفته شده‌است.
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
در ساختار درونی سیستم ASP.NET Core Identity، در عمل چیزی به نام Role وجود خارجی ندارد. برای مثال Roleها هم در این سیستم یک User-Claim از نوع ClaimTypes.Role هستند. تمام سیستم Identity بر اساس User Claims کار می‌کند. تمام Roleها و غیره در پشت صحنه ابتدا تبدیل به user claims می‌شوند و سپس استفاده خواهند شد.
var roles = ((ClaimsIdentity)User.Identity).Claims
                .Where(c => c.Type == ClaimTypes.Role)
                .Select(c => c.Value);
بنابراین برای سفارشی سازی‌های ویژه، نیازی به دستکاری در ساختار جداول پیش‌فرض آن نیست. برای اینکار از جدول ویژه‌ی AppUserClaims و برای طراحی Policy ویژه‌ی پردازش آن، از AuthorizationHandler توضیح داده شده‌ی در مطلب جاری ایده بگیرید.

نظرات مطالب
تنظیمات JSON در ASP.NET Web API
سلام و وقت بخیر
من وقتی میخوام اطلاعات یک فایل جیسون رو به آبجکت تبدیل کنم، با این خطا مواجه میشم:
Additional text encountered after finished reading JSON content: ,. Path '', line 1, position 6982. 
بعد از جستجو متوجه شدم که خطا به دلیل وجود کرکترهای کنترلی هست، پس فایل مذکور رو با روشهای زیر (هر کدام رو جداگانه تست کردم) تمیز کردم:
        public string RemoveControlCharacters1(string input)
        {
            string output = Regex.Replace(input, @"[\u0000-\u001F]", string.Empty);
            return output;
        }

        public string RemoveControlCharacters2(string input)
        {
            string output = new string(input.Where(c => !char.IsControl(c)).ToArray());
            return output;
        }

        public string RemoveControlCharacters3(string inString)
        {
            if (inString == null) return null;
            StringBuilder newString = new StringBuilder();
            char ch;
            for (int i = 0; i < inString.Length; i++)
            {
                ch = inString[i];
                if (!char.IsControl(ch))
                {
                    newString.Append(ch);
                }
            }
            return newString.ToString();
        }
اما کماکان همان خطا را در زمان اجر میبینم.
آیا مشکل چیز دیگری است؟
پرسش: چطور میشود به جیسون دات نت گفت که اصلا کرکترهای کنترلی و یا چیزهایی را که ممکن است خطا ایجاد کنند، ندید بگیرد؟
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
تا اینجا تمام قسمت‌های این سری، برای اساس اطلاعات یک کلاس Config استاتیک تشکیل شده‌ی در حافظه ارائه شدند. این روش برای دمو و توضیح مفاهیم پایه‌ی IdentityServer بسیار مفید است؛ اما برای دنیای واقعی خیر. بنابراین در ادامه می‌خواهیم این قسمت را با اطلاعات ذخیره شده‌ی در بانک اطلاعاتی تعویض کنیم. یک روش مدیریت آن، نصب ASP.NET Core Identity دقیقا داخل همان پروژه‌ی IDP است. در این حالت کدهای ASP.NET Core Identity مایکروسافت، کار مدیریت کاربران IDP را انجام می‌دهند. روش دیگر اینکار را که در اینجا بررسی خواهیم کرد، تغییر کدهای Quick Start UI اضافه شده‌ی در «قسمت چهارم - نصب و راه اندازی IdentityServer»، جهت پذیرفتن مدیریت کاربران مبتنی بر بانک اطلاعاتی تهیه شده‌ی توسط خودمان است. مزیت آن آشنا شدن بیشتر با کدهای Quick Start UI و درک زیرساخت آن است.


تکمیل ساختار پروژه‌ی IDP

تا اینجا برای IDP، یک پروژه‌ی خالی وب را ایجاد و به مرور، آن‌را تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژه‌ی Class library کمکی را نیز به Solution آن اضافه می‌کنیم:


- DNT.IDP.DomainClasses
در این پروژه، کلاس‌های متناظر با موجودیت‌های جداول مرتبط با اطلاعات کاربران قرار می‌گیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آن‌را تشکیل می‌دهد. همچنین به همراه تنظیمات و Seed اولیه‌ی اطلاعات بانک اطلاعاتی نیز می‌باشد.
رشته‌ی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شده‌است.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژه‌ی مشترک بین چند پروژه‌ی دیگر قرار گرفته‌است. از آن جهت هش کردن کلمات عبور، در دو پروژه‌ی DataLayer و همچنین Services استفاده می‌کنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار می‌کند، در این پروژه قرار گرفته‌است.


ساختار بانک اطلاعاتی کاربران IdentityServer

در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آن‌ها تشکیل می‌شود:
namespace DNT.IDP.DomainClasses
{
    public class User
    {
        [Key]
        [MaxLength(50)]       
        public string SubjectId { get; set; }
    
        [MaxLength(100)]
        [Required]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }

        [Required]
        public bool IsActive { get; set; }

        public ICollection<UserClaim> UserClaims { get; set; }

        public ICollection<UserLogin> UserLogins { get; set; }
    }
}
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار می‌رود.
ساختار Claims او نیز به صورت زیر تعریف می‌شود که با تعریف یک Claim استاندارد، سازگاری دارد:
namespace DNT.IDP.DomainClasses
{
    public class UserClaim
    {         
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimType { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimValue { get; set; }
    }
}
همچنین کاربر می‌توان تعدادی لاگین نیز داشته باشد:
namespace DNT.IDP.DomainClasses
{
    public class UserLogin
    {
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string LoginProvider { get; set; }

        [Required]
        [MaxLength(250)]
        public string ProviderKey { get; set; }
    }
}
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آن‌ها است.

در پروژه‌ی DNT.IDP.DataLayer در پوشه‌ی Configurations آن، کلاس‌های UserConfiguration و UserClaimConfiguration را مشاهده می‌کنید که حاوی اطلاعات اولیه‌ای برای تشکیل User 1 و User 2 به همراه Claims آن‌ها هستند. این اطلاعات را دقیقا از فایل استاتیک ‍Config که در قسمت‌های قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کرده‌ایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونه‌ی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار می‌کند، چنین ساختاری را دارد:
    public interface IUsersService
    {
        Task<bool> AreUserCredentialsValidAsync(string username, string password);
        Task<User> GetUserByEmailAsync(string email);
        Task<User> GetUserByProviderAsync(string loginProvider, string providerKey);
        Task<User> GetUserBySubjectIdAsync(string subjectId);
        Task<User> GetUserByUsernameAsync(string username);
        Task<IEnumerable<UserClaim>> GetUserClaimsBySubjectIdAsync(string subjectId);
        Task<IEnumerable<UserLogin>> GetUserLoginsBySubjectIdAsync(string subjectId);
        Task<bool> IsUserActiveAsync(string subjectId);
        Task AddUserAsync(User user);
        Task AddUserLoginAsync(string subjectId, string loginProvider, string providerKey);
        Task AddUserClaimAsync(string subjectId, string claimType, string claimValue);
    }
که توسط آن امکان دسترسی به یک کاربر، اطلاعات Claims او و افزودن رکوردهایی جدید وجود دارد.
تنظیمات نهایی این سرویس‌ها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگی‌ها، صورت گرفته‌اند. همچنین در اینجا متد initializeDb را نیز مشاهده می‌کنید که با فراخوانی متد context.Database.Migrate، تمام کلاس‌های Migrations پروژه‌ی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند.


غیرفعال کردن صفحه‌ی Consent در Quick Start UI

در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایل‌های Quick Start UI را به پروژه‌ی IDP اضافه کردیم. در ادامه می‌خواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحه‌ی Consent در Quick Start UI، لیست scopes درخواستی برنامه‌ی کلاینت ذکر شده و سپس کاربر انتخاب می‌کند که کدامیک از آن‌ها، باید به برنامه‌ی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامه‌های کلاینت توسط ما توسعه یافته‌اند، بی‌معنا است و صرفا برای کلاینت‌های ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا می‌کند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.


تغییر نام پوشه‌ی Quickstart و سپس اصلاح فضای نام پیش‌فرض کنترلرهای آن

در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشه‌ی Quickstart برنامه‌ی IDP قرار گرفته‌اند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشه‌ی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیش‌فرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژه‌ی جاری تغییر می‌دهیم. برای مثال کنترلر Account واقع در پوشه‌ی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترل‌ها عمل می‌کنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آن‌را نیز کامپایل می‌کند و اگر خطایی در آن‌ها وجود داشته باشد، امکان بررسی و رفع آن‌ها پیش از اجرای برنامه، میسر است.
و یا می‌توان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
@using DNT.IDP.Controllers.Account;
@using DNT.IDP.Controllers.Consent;
@using DNT.IDP.Controllers.Grants;
@using DNT.IDP.Controllers.Home;
@using DNT.IDP.Controllers.Diagnostics;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


تعامل با IdentityServer از طریق کدهای سفارشی

پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویس‌های متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده می‌کند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شده‌ی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظه‌ای برنامه را از متد ()Config.GetUsers دریافت می‌کنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی می‌کنیم:
 public static class IdentityServerBuilderExtensions
 {
        public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users)
        {
            builder.Services.AddSingleton(new TestUserStore(users));
            builder.AddProfileService<TestUserProfileService>();
            builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>();

            return builder;
        }
}
- ابتدا یک TestUserStore را به صورت Singleton ثبت کرده‌است.
- سپس سرویس پروفایل کاربران را اضافه کرده‌است. این سرویس با پیاده سازی اینترفیس IProfileService تهیه می‌شود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آن‌ها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمه‌ی عبور و نام کاربری را در صورت استفاده‌ی از Flow ویژه‌ای به نام ResourceOwner که استفاده‌ی از آن توصیه نمی‌شود (ROBC Flow)، انجام می‌دهد.

برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشه‌ی پروژه‌ی IDP با محتوای ذیل اضافه می‌کنیم:
using DNT.IDP.Services;
using Microsoft.Extensions.DependencyInjection;

namespace DNT.IDP
{
    public static class IdentityServerBuilderExtensions
    {
        public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
        {
            // builder.Services.AddScoped<IUsersService, UsersService>();
            builder.AddProfileService<CustomUserProfileService>();
            return builder;
        }
    }
}
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شده‌است که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار می‌کند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شده‌است. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شده‌است.
سپس یک ProfileService سفارشی را ثبت کرده‌ایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی می‌شود:
namespace DNT.IDP.Services
{
    public class CustomUserProfileService : IProfileService
    {
        private readonly IUsersService _usersService;

        public CustomUserProfileService(IUsersService usersService)
        {
            _usersService = usersService;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId);
            context.IssuedClaims = claimsForUser.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            context.IsActive = await _usersService.IsUserActiveAsync(subjectId);
        }
    }
}
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آن‌را تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا می‌کند.
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن می‌توان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفاده‌های بعدی، ذخیره کرد.

اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddCustomUserStore()
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
تا اینجا فقط این سرویس‌های جدید را ثبت کرده‌ایم، اما هنوز کار خاصی را انجام نمی‌دهند و باید از آن‌ها در برنامه استفاده کرد.


اتصال IdentityServer به User Store سفارشی

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

            _interaction = interaction;
            _clientStore = clientStore;
            _schemeProvider = schemeProvider;
            _events = events;
        }
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4‌  را انجام می‌دهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینت‌ها را ارائه می‌دهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار می‌گیرد.
- IEventService رخ‌دادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش می‌دهد.
- در آخر، TestUserStore تزریق شده‌است که می‌خواهیم آن‌را با User Store سفارشی خودمان جایگزین کنیم.  بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین می‌کنیم:
        private readonly TestUserStore _users;
        private readonly IUsersService _usersService;
        public AccountController(
    // ...
            IUsersService usersService)
        {
            _usersService = usersService;
    // ...
        }
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه می‌داریم. از این جهت که قسمت‌های لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده می‌کنند و آن‌را در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده می‌کنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمه‌ی عبور و همچنین یافتن کاربر متناظر با آن:
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
    //...
            if (ModelState.IsValid)
            {
                if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password))
                {
                    var user = await _usersService.GetUserByUsernameAsync(model.Username);
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شده‌ی به سیستم نمایش داده می‌شوند که بیانگر صحت عملکرد CustomUserProfileService است.


افزودن امکان ثبت کاربران جدید به برنامه‌ی IDP

پس از اتصال قسمت login برنامه‌ی IDP به بانک اطلاعاتی، اکنون می‌خواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبت‌نام را تشکیل می‌دهد، ابتدا با نام کاربری و کلمه‌ی عبور شروع می‌شود:
    public class RegisterUserViewModel
    {
        // credentials       
        [MaxLength(100)]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }
سپس سایر خواصی که در اینجا اضافه می‌شوند:
    public class RegisterUserViewModel
    {
   // ...

        // claims 
        [Required]
        [MaxLength(100)]
        public string Firstname { get; set; }

        [Required]
        [MaxLength(100)]
        public string Lastname { get; set; }

        [Required]
        [MaxLength(150)]
        public string Email { get; set; }

        [Required]
        [MaxLength(200)]
        public string Address { get; set; }

        [Required]
        [MaxLength(2)]
        public string Country { get; set; }
در کنترلر UserRegistrationController، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل می‌دهد. ابتدا نام کاربری و کلمه‌ی عبور را در جدول کاربران ثبت می‌کند و سپس سایر خواص این ViewModel را در جدول UserClaims:
varuserToCreate=newUser
{
  Password=model.Password.GetSha256Hash(),
  Username=model.Username,
  IsActive=true
};
userToCreate.UserClaims.Add(newUserClaim("country",model.Country));
userToCreate.UserClaims.Add(newUserClaim("address",model.Address));
userToCreate.UserClaims.Add(newUserClaim("given_name",model.Firstname));
userToCreate.UserClaims.Add(newUserClaim("family_name",model.Lastname));
userToCreate.UserClaims.Add(newUserClaim("email",model.Email));
userToCreate.UserClaims.Add(newUserClaim("subscriptionlevel","FreeUser"));
ج) افزودن RegisterUser.cshtml
این فایل، view متناظر با ViewModel فوق را ارائه می‌دهد که توسط آن، کاربری می‌تواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده می‌شود. به همین جهت بهتر است فضای نام آن‌را به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحه‌ی لاگین در Login.cshtml
این لینک دقیقا در ذیل چک‌باکس Remember My Login اضافه شده‌است.


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


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


برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شده‌است، این کاربر یکسری از لینک‌های برنامه‌ی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
Protocol Buffers فرمتی برای تبادل دیتا
Protocol Buffers  فرمتی جدید برای تبادل دیتا بین سرور و کلاینت میباشد که توسط گوگل طراحی و ساخته شده است و همچنین اکثر زیرساخت‌های گوگل از این فرمت برای تبادل اطلاعات بین سرویس‌ها استفاده میکنند. Protocol Buffer را میتوان به عنوان جایگزینی برای JSON/XML بکار برد و به دلایل زیادی که در ادامه درباره‌ی آن صحبت میکنیم میتواند گزینه‌ی مناسبی برای Microservices‌ها باشد و همچنین سرعت بالا، سادگی در استفاده، پشتیبانی از زبان‌های برنامه نویسی متعدد از ویژگی‌های منحصر به فرد این زبان برای تبادل اطلاعات است.
در ابتدا میخواهم کمی راجع به تبادل دیتا، از گذشته تا به حال صحبت کنم:
مدت‌ها است از csv‌ها برای تبادل اطلاعات استفاده میشود؛ اما مزایا و معایب خاص خود را دارد، از جمله اینکه parse کردن راحتی دارد، راحتی در خواندن و غیره. معایبش هم این‌است که گارانتی برای نوع type ندارد و اینکه المان‌هایی که حاوی کاما هستند با مشکل رو به رو میشوند و غیره...
بعد از آن دیتابیس‌ها وارد کار شدند که همه‌ی ما کم و بیش با آنها آشنا هستیم؛ در آن‌ها دیتا‌ها کاملا با type مشخصی هستند و اینکه در table‌های مجزا ذخیره میشوند. مشکلاتش هم این است که دیتا باید حتما flat باشد و اینکه بین دیتابیس‌های مختلف definition‌های مختلفی وجود دارد.
بعد از آن با JSON آشنایی داریم که مزایای زیادی دارد و مدت هاست که مورد استفاده قرار گرفته و شامل این‌است که دیتا در آن میتواند تو در تو ذخیره شود، آرایه داشته باشد، کاملا در دنیای وب مورد قبول واقع شده، به وسیله‌ی هر زبانی قابل خواندن‌است و اینکه خیلی راحت در شبکه قابل انتقال می‌باشد. معایبش هم این‌است که خیلی راحت میتواند خیلی بزرگ شود و اینکه قابلیت کامنت، متادیتا و داکیومنتیشن هم ندارد.
اما میرسیم به گزینه‌ی آخر که protocol buffers است و ابزاری هست که ممکن است خیلی‌ها با آن آشنا نباشند. قبل از بررسی دقیقش به مزایا و معایبش می‌پردازیم. مزایا آن این‌است که دیتا در آن کاملا typed میباشد. دیتای آن به صورت اتوماتیک compressed می‌شود. اسکیما در آن توسط زبان منحصر به فردش قابل تعریف است و توسط تقریبا همه‌ی زبان‌های برنامه نویسی مشهور قابل استفاده‌است. تغییرات اسکیما در آن کنترل شده‌است. 3 تا 10 بار کم حجم‌تر و 20 تا 100 بار سریعتر از xml است و اینکه از روی آن می‌توان کد آماده برای استفاده تولید کرد که سرعت برنامه نویسی را خیلی بالا می‌برد. از مشکلاتش هم این است که ممکن است در یک سری از زبان‌های برنامه نویسی خاص قابل استفاده نباشد. البته بر روی C#, Nodejs, C, Go, Python ,... به خوبی کار می‌کند. مشکل دیگرش هم این‌است که نمی‌شود فایلش را با ادیتورها باز کرد و قابل خواندن نیست؛ چون serialized و compressed شده‌است.

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

ابتدا از طریق فرمت protocol buffer، فایل‌های خود را که قرار است انتقال داده شوند، مینویسیم.

سپس بصورت خودکار برای زبان برنامه نویسی مطبوع خود آن را generate میکنیم.

کد‌های تولید شده بصورت خودکار و کاملا آماده هستند و ضمن اینکه encode/decode شدن بصورت خودکار توسط فریم ورک انجام شده و قابلیت تعامل بین زبان‌های مختلف برنامه نویسی یا سرویس‌های مختلف برقرار است.


نکته:

  •  بعضی از دیتابیس‌ها از فرمت protocol buffers پشتیبانی میکنند.
  • اکثر فریم ورک‌های RPC شامل gRPC از پروتکل بافر برای تبادل دیتا استفاده میکنند.
  • گوگل برای تمام سرویس‌های داخلی خود از آن استفاده میکند.
  • بعضی از پروژه‌های خیلی بزرگ مثل etcd از پروتکل بافر برای تبادل دیتا استفاده میکنند.
  • ما در این مقاله از ورژن 3 پروتکل بافر استفاده میکنیم.


نصب Code generator

برای اینکه بتوانیم از طریق فایل‌هایی که میسازیم کد‌های generate شده را تولید کنیم، احتیاج به کامپایلر مربوطه را داریم.

اگر از MacOSX استفاده میکنید، به راحتی با استفاده از دستور زیر می‌توانید آن را نصب کنید:

brew install protobuf

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

https://github.com/google/protobuf/releases
https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-win32.zip

حالا میخواهیم اولین فایل خود را با این فرمت بسازیم.

اول از همه با هم نگاهی به ساختار فایل مربوطه میاندازیم:

همانطور که در تصویر فوق می‌بینید، همه چیز به سادگی مشخص است؛ ورژن 3 که آخرین ورژن پروتکل بافر میباشد، آیتمی به نام MyMessage با پراپرتی‌هایی مشخص شده از Type بخصوص، تعریف شده‌اند، تگ‌ها هم باید به ترتیب وارد شده باشند.

حالا میخواهیم بصورت واقعی protocol buffer خود را طراحی کرده و سپس از روی آن کد‌های مربوطه را generate نماییم؛ به نام sample.proto بصورت زیر:

syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

در فایل فوق علاوه بر تعریف‌های اولیه، یک سرویس را هم اضافه کرده‌ایم و همچنین متدی را با ورودی و خروجی‌های مشخصی ایجاد کرده‌ایم (امکانات پروتکل بافر خیلی بیشتر از این موارد است؛ از جمله فرمت‌های آرایه و غیره را نیز پشتیبانی میکند، همچنین از روشی برای versioning استفاده میکند که obsolete کردن پراپرتی‌ها و نسخه بندی را بسیار راحت می‌کند و ...). به سادگی قابلیت طراحی و پیاده سازی سرور و کلاینت مربوط به این آیتم ایجاد شده با استفاده از زبان‌های برنامه نویسی مختلف فراهم میباشد. حال کافی‌است که پروتکل بافر خود را با زبان دلخواه خود generate کنیم. در قسمت زیر برای زبان‌های برنامه نویسی Go و #C، کد‌ها را تولید میکنیم.

protoc sample.proto --go_out=plugins=grpc:.  
protoc sample.proto --csharp_out=.

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

در مقاله‌ی بعدی به آشنایی با gRPC می‌پردازیم و ضمن اینکه یک سرور با #C و یک کلاینت با زبان برنامه نویسی Go را نوشته که از طریق پروتکل بافر با هم به تبادل اطلاعات می‌پردازند!

مطالب
Globalization در ASP.NET MVC - قسمت سوم
قبل از ادامه، بهتر است یک مقدمه کوتاه درباره انواع منابع موجود در ASP.NET ارائه شود تا درک مطالب بعدی آسانتر شود.

نکات اولیه
- یک فایل Resource درواقع یک فایل XML شامل رشته هایی برای ذخیره سازی مقادیر (منابع) موردنیاز است. مثلا رشته هایی برای ترجمه به زبانهای دیگر، یا مسیرهایی برای یافتن تصاویر یا فایلها و ... . پسوند این فایلها resx. است (مثل MyResource.resx).
- این فایلها برای ذخیره منابع از جفت داده‌های کلید-مقدار (key-value pair) استفاده می‌کنند. هر کلید معرف یک ورودی مجزاست. نام این کلیدها حساس به حروف بزرگ و کوچک نیست (Not Case-Sensitive).
- برای هر زبان (مثل fa برای فارسی) یا کالچر موردنظر (مثل fa-IR برای فارسی ایرانی) می‌توان یک فایل Resource جداگانه تولید کرد. عناون زبان یا کالچر باید جزئی از نام فایل Resource مربوطه باشد (مثل MyResource.fa.resx یا MyResource.fa-IR.resx). هر منبع باید دارای یک فایل اصلی (پیش‌فرض) Resource باشد. این فایل، فایلی است که برای حالت پیش‌فرض برنامه (بدون کالچر) تهیه شده است و در عنوان آن از نام زیان یا کالچری استفاده نشده است (مثل MyResource.resx). برای اطلاعات بیشتر به قسمت اول این سری مراجعه کنید.
- تمامی فایل‌های Resource باید دارای کلیدهای یکسان با فایل اصلی Resource باشند. البته لزومی ندارد که این فایل‌ها حاوی تمامی کلیدهای منبع پیش‌فرض باشند. درصورت عدم وجود کلیدی در یک فایل Resource عملیات پیش فرض موجود در دات نت با استفاده از فرایند مشهور به fallback مقدار کلید موردنظر را از نزدیکترین و مناسبترین فایل موجود انتخاب می‌کند (درباره این رفتار در قسمت اول توضیحاتی ارائه شده است).
- در زمان اجرا موتور پیش فرض مدیریت منابع دات نت با توجه به کالچر UI در ثرد جاری اقدام به انتخاب مقدار مناسب برای کلیدهای درخواستی (به همراه فرایند fallback) می‌کند. فرایند نسبتا پیچیده fallback در اینجا شرح داده شده است.

منابع Global و Local
در ASP.NET دو نوع کلی Resource وجود دارد که هر کدام برای موقعیت‌های خاصی مورد استفاده قرار می‌گیرند:

- Resourceهای Global: منابعی کلی هستند که در تمام برنامه در دسترسند. این فایل‌ها در مسیر رزرو شده APP_GlobalResources در ریشه سایت قرار می‌گیرند. محتوای هر فایل resx. موجود در این فولدر دارای دسترسی کلی خواهد بود.

- Resourceهای Local: این منابع همان‌طور که از نامشان پیداست محلی! هستند و درواقع مخصوص همان مسیری هستند که در آن تعبیه شده اند! در استفاده از منابع محلی به ازای هر صفحه وب (aspx. یا master.) یا هر یوزرکنترل (ascx.) یک فایل resx. تولید می‌شود که تنها در همان صفحه یا یوزرکنترل در دسترس است. این فایل‌ها درون فولدر رزرو شده APP_LocalResources در مسیرهای موردنظر قرار می‌گیرند. درواقع در هر مسیری که نیاز به این نوع از منابع باشد، باید فولدری با عنوان App_LocalResources ایجاد شود و فایلهای resx. مرتبط با صفحه‌ها یا یوزرکنترل‌های آن مسیر در این فولدر مخصوص قرار گیرد.
در تصویر زیر چگونگی افزودن این فولدرهای مخصوص به پروژه وب اپلیکیشن نشان داده شده است:

نکته: دقت کنید که تنها یک فولدر App_GlobalResources به هر پروزه می‌توان افزود. همچنین در ریشه هر مسیر موجود در پروژه تنها می‌توان یک فولدر Appp_LocalResources داشت. پس از افزودن هر یک از این فولدرهای مخصوص، منوی فوق به صورت زیر در خواهد آمد:

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

برای روشن‌تر شدن مطالب اشاره شده در بالا به تصویر فرضی! زیر توجه کنید (اسمبلی‌های تولید شده برای منابع کلی و محلی فرضی است):

در تصویر بالا محل قرارگیری انواع مختلف فایلهای Resource و نیز محل نهایی فرضی اسمبلی‌های ستلایت تولید شده، برای حداقل یک زبان غیر از زبان پیش فرض برنامه، نشان داده شده است.

نکته: نحوه برخورد با این نوع از فایل‌های Resource در پروژه‌های Web Site و Web Application کمی باهم فرق می‌کند. موارد اشاره شده در این مطلب بیشتر درباره Web Applicationها صدق می‌کند.

برای آشنایی بیشتر بهتر است یک برنامه وب اپلیکیشن جدید ایجاد کرده و همانند تصویر زیر یکسری فایل Resource به فولدرهای اشاره شده در بالا اضافه کنید:

همانطور که مشاهده می‌کنید به صورت پیش‌فرض برای منابع کلی یک فایل cs. تولید می‌شود. اما اثری از این فایل برای منابع محلی نیست.
حال اگر پنجره پراپرتی فایل منبع کلی را باز نمایید با چیزی شبیه به تصویر زیر مواجه خواهید شد:

می‌بینید که خاصیت Build Action آن به Content مقداردهی شده است. این مقدار موجب می‌شود تا این فایل به همین صورت و در همین مسیر مستقیما در پابلیش نهایی برنامه ظاهر شود. در قسمت قبل به خاصیت Buil Action و مقادیر مختلف آن اشاره شده است.
هم‌چنین می‌بینید که مقدار پراپرتی Custom Tool به GlobalResourceProxyGenerator تنظیم شده است. این ابزار مخصوص تولید کلاس مربوط به منابع کلی در ویژوال استودیو است. با استفاده از این ابزار فایل Resource1.Designer.cs که در تصویر قبلی نیز نشان داده شده، تولید می‌شود.
حالا پنجره پراپرتی‌های منبع محلی را باز کنید:

می‌بینید که همانند منبع کلی خاصیت Build Action آن به Content تنظیم شده است. همچنین مقداری برای پراپرتی Custom Tool تنظیم نشده است. این مقدار پیش فرض را تغییر ندهید، چون با تنظیم مقداری برای آن چیز مفیدی عایدتان نمی‌شود! 

نکته: برای به روز رسانی مقادیر کلیدهای منابعی که با توجه به توضیحات بالا به همراه برنامه به صورت فایلهای resx. پابلیش می‌شوند، کافی است تا محتوای فایلهای resx. مربوطه با استفاده از یک ابزار (همانند نمونه ای که در قسمت قبل شرح داده شد) تغییر داده شوند. بقیه عملیات توسط ASP.NET انجام خواهد شد. اما با تغییر محتوای این فایلهای resx. با توجه به رفتار FCN در ASP.NET (که در قسمت قبل نیز توضیح داده شد) سایت Restart خواهد شد. البته این روش تنها برای منابع کلی و محلی درون مسیرهای مخصوص اشاره شده کار خواهد کرد.

استفاده از منابع Local و Global
پس از تولید فایل‌های Resource، می‌توان از آن‌ها در صفحات وب استفاده کرد. معمولا از این نوع منابع برای مقداردهی پراپرتی کنترل‌ها در صفحات وب استفاده می‌شود. برای استفاده از کلیدهای منابع محلی می‌توان از روشی همانند زیر بهره برد:
<asp:Label ID="lblLocal" runat="server" meta:resourcekey="lblLocalResources" ></asp:Label> 
اما برای منابع کلی تنها می‌توان از روش زیر استفاده کرد (یعنی برای منابع محلی نیز می‌توان از این روش استفاده کرد):
<asp:Label ID="lblGlobal" runat="server" Text="<%$ Resources:CommonTerms, HelloText %>" ></asp:Label> 
به این عبارات که با فوت پررنگ مشخص شده اند اصطلاحا «عبارات بومی‌سازی» (Localization Expression) می‌گویند. در ادامه این سری مطالب با نحوه تعریف نمونه‌های سفارشی آن آشنا خواهیم شد.
به نمونه اول که برای منابع محلی استفاده می‌شود نوع ضمنی (Implicit Localization Expression) می‌گویند. زیرا نیازی نیست تا محل کلید موردنظر صراحتا ذکر شود!
به نمونه دوم که برای منابع کلی استفاه می‌شود نوع صریح (Explicit Localization Expression) می‌گویند. زیرا برای یافتن کلید موردنظر باید آدرس دقیق آن ذکر شود!

بومی سازی ضمنی (Implicit Localization) با منابع محلی
عنوان کلید مربوطه در این نوع عبارات همانطور که در بالا نشان داده شده است، با استفاده از پراپرتی مخصوص meta:resoursekey مشخص می‌شود. در استفاده از منابع محلی تنها یک نام برای کل خواص کنترل مربوطه در صفحات وب کفایت می‌کند. زیرا عنوان کلیدهای این منبع باید از طرح زیر پیروی کند:
ResourceKey.Property
ResourceKey.Property.SubProperty    یا    ResourceKey.Property-SubProperty
برای مثال در لیبل بالا که نام کلید Resource آن به lblLocalResources تنظیم شده است، اگر نام صفحه وب مربوطه page1.aspx باشد، برای تنظیم خواص آن در فایل page1.aspx.resx مربوطه باید از کلیدهایی با عناوینی مثل عنوان‌های زیر استفاه کرد:
lblLocalResources.Text
lblLocalResources.BackColor
برای نمونه به تصاویر زیر دقت کنید:


بومی سازی صریح (Explicit Localization)
در استفاده از این نوع عبارات، پراپرتی مربوطه و نام فایل منبع صراحتا در تگ کنترل مربوطه آورده می‌شود. بنابراین برای هر خاصیتی که می‌خواهیم مقدار آن از منبعی خاص گرفته شود باید از عبارتی با طرح زیر استفاده کنیم:
<%$ Resources: Class, ResourceKey %>
در این عبارت، رشته Resources پیشوند (Prefix) نام دارد و مشخص کننده استفاده از نوع صریح عبارات بومی سازی است. Class نام کلاس مربوط به فایل منبع بوده و اختیاری است که تنها برای منابع کلی باید آورده شود. ResourceKey نیز کلید مربوطه را در فایل منبع مشخص می‌کند.
برای نمونه به تصاویر زیر دقت کنید:


نکته: استفاده همزمان از این دو نوع عبارت بومی سازی در یک کنترل مجاز نیست!

نکته: به دلیل تولید کلاسی مخصوص منابع کلی (با توجه به توضیحات ابتدای این مطلب راجع به پراپرتی Custom Tool)، امکان استفاده مستقیم از آن درون کد نیز وجود دارد. این کلاسها که به صورت خودکار تولید می‌شوند، به صورت مستقیم از کلاس ResourceManager برای یافتن کلیدهای منابع استفاده می‌کنند. اما روش مستقیمی برای استفاده از کلیدهای منابع محلی درون کد وجود ندارد. 

نکته: درون کلاس System.Web.UI.TemplateControl و نیز کلاس HttpContext دو متد با نامهای GetGlobalResourceObject و GetLocalResourceObject وجود دارد که برای یافتن کلیدهای منابع به صورت غیرمستقیم استفاده می‌شوند. مقدار برگشتی این دو متد از نوع object است. این دو متد به صورت مستقیم از کلاس ResourceManager استفاده نمیکنند! هم‌چنین ازآنجاکه کلاس Page از کلاس TemplateControl مشتق شده است، بنابراین این دو متد در صفحات وب در دسترس هستند.

دسترسی با برنامه نویسی
همانطور که در بالا اشاره شد امکان دستیابی به کلیدهای منابع محلی و کلی ازطریق دو متد GetGlobalResourceObject و GetLocalResourceObject نیز امکان پذیر است. این دو متد با فراخوانی ResourceProviderFactory جاری سعی در یافتن مقادیر کلیدهای درخواستی در منابع موجود می‌کنند. درباره این فرایند در مطالب بعدی به صورت مفصل بحث خواهد شد.

کلاس TemplateControl
این دو متد در کلاس TemplateControl از نوع Instance (غیر استاتیک) هستند. امضای (Signature) این دو متد در این کلاس به صورت زیر است:

متد GetLocalResourceObject:
protected object GetLocalResourceObject(string resourceKey)
protected object GetLocalResourceObject(string resourceKey, Type objType, string propName)
در متد اول، پارامتر resourceKey در متد GetLocalResourceObject معرف کلید منبع مربوطه در فایل منبع محلی متناظر با صفحه جاری است. مثلا lblLocalResources.Text. ازآنجاکه به صورت پیش‌فرض موقعیت فایل منبع محلی مرتبط با صفحات وب مشخص است بنابراین تنها ارائه کلید مربوطه برای یافتن مقدار آن کافی است. مثال:
txtTest.Text = GetLocalResourceObject("txtTest.Text") as string;
متد دوم برای استخراج کلیدهای منبع محلی با مشخص کردن نوع داده محتوا (معمولا برای داده‌های غیر رشته‌ای) و پراپرتی موردنظر به کار می‌رود. در این متد پارامتر objType برای معرفی نوع داده متناظر با داده موجود در کلید resourceKey استفاده می‌شود. از پارامتر propName نیز همانطور که از نامش پیداست برای مشخص کردن پراپرتی موردنظر از این نوع داده معرفی شده استفاده می‌شود.

متد GetGlobalResourceObject:
protected object GetGlobalResourceObject(string className, string resourceKey)
protected object GetGlobalResourceObject(string className, string resourceKey, Type objType, string propName)
در این دو متد، پارامتر className مشخص کننده نام کلاس متناظر با فایل منبع اصلی (فایل منبع اصلی که کلاس مربوطه با نام آن ساخته می‌شود) است. سایر پارامترها همانند دو متد قبلی است. مثال:
TextBox1.Text = GetGlobalResourceObject("Resource1", "String1") as string;

کلاس HttpContext
در این کلاس دو متد موردبحث از نوع استاتیک و به صورت زیر تعریف شده‌اند:

متد GetLocalResourceObject: 
public static object GetLocalResourceObject(string virtualPath, string resourceKey)
public static object GetLocalResourceObject(string virtualPath, string resourceKey, CultureInfo culture)
در این دو متد، پارامتر virtualPath مشخص کننده مسیر نسبی صفحه وب متناظر با فایل منبع محلی موردنظر است، مثل "Default.aspx/~". پارامتر resourceKey نیز کلید منبع را تعیین می‌کند و پارامتر culture نیز به کالچر موردنظر اشاره دارد. مثال:
txtTest.Text = HttpContext.GetLocalResourceObject("~/Default.aspx", "txtTest.Text") as string;
 
متد GetGlobalResourceObject:
public static object GetGlobalResourceObject(string classKey, string resourceKey)
public static object GetGlobalResourceObject(string classKey, string resourceKey, CultureInfo culture)
در این دو متد، پارامتر className مشخص کننده نام کلاس متناظر با فایل منبع اصلی (فایل منبع بدون نام زبان که کلاس مربوطه با نام آن ساخته می‌شود) است. سایر پارامترها همانند دو متد قبلی است. مثال:
TextBox1.Text = HttpContext.GetGlobalResourceObject("Resource1", "String1") as string;
 
نکته: بدیهی است که در MVC تنها می‌توان از متدهای کلاس HttpContext استفاده کرد.

روش دیگری که تنها برای منابع کلی در دسترس است، استفاده مستقیم از کلاسی است که به صورت خودکار توسط ابزارهای Visual Studio برای فایل منبع اصلی تولید می‌شود. نمونه‌ای از این کلاس را که برای یک فایل Resource1.resx (که تنها یک ورودی با نام String1 دارد) در پوشه App_GlobalResources تولید شده است، در زیر مشاهده می‌کنید:
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.17626
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace Resources {
    using System;
    
    /// <summary>
    ///   A strongly-typed resource class, for looking up localized strings, etc.
    /// </summary>
    // This class was auto-generated by the StronglyTypedResourceBuilder
    // class via a tool like ResGen or Visual Studio.
    // To add or remove a member, edit your .ResX file then rerun ResGen
    // with the /str option or rebuild the Visual Studio project.
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Web.Application.StronglyTypedResourceProxyBuilder", "10.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resource1 {
        
        private static global::System.Resources.ResourceManager resourceMan;
        
        private static global::System.Globalization.CultureInfo resourceCulture;
        
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resource1() {
        }
        
        /// <summary>
        ///   Returns the cached ResourceManager instance used by this class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if (object.ReferenceEquals(resourceMan, null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Resources.Resource1", global::System.Reflection.Assembly.Load("App_GlobalResources"));
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }
        
        /// <summary>
        ///   Overrides the current thread's CurrentUICulture property for all
        ///   resource lookups using this strongly typed resource class.
        /// </summary>
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }
        
        /// <summary>
        ///   Looks up a localized string similar to String1.
        /// </summary>
        internal static string String1 {
            get {
                return ResourceManager.GetString("String1", resourceCulture);
            }
        }
    }
}

نکته: فضای نام پیش‌فرض برای منابع کلی در این کلاس‌ها همیشه Resources است که برابر پیشوند (Prefix) عبارت بومی سازی صریح است.

نکته: در کلاس بالا نحوه نمونه سازی کلاس ResourceManager نشان داده شده است. همانطور که مشاهده می‌کنید تعیین کردن مشخصات فایل اصلی Resource مربوطه که در اسمبلی نهایی تولید و کش می‌شود، اجباری است! در مطلب بعدی با این کلاس بیشتر آشنا خواهیم شد.

نکته: همانطور که قبلا نیز اشاره شد، کار تولید اسمبلی مربوط به فایل‌های منابع کلی و محلی و کش کردن آن‌ها در اسمبلی در زمان اجرا کاملا بر عهده ASP.NET است. مثلا در نمونه کد بالا می‌بینید که کلاس ResourceManager برای استخراج نوع Resources.Resource1 از اسمبلی App_GlobalResources نمونه‌سازی شده است، با اینکه این اسمبلی و نوع مذبور در زمان کامپایل و پابلیش وجود ندارد!

برای استفاده از این کلاس می‌توان به صورت زیر عمل کرد:
TextBox1.Text = Resources.Resource1.String1;

نکته: همانطور که قبلا هم اشاره شد، متاسفانه روش بالا (برخلاف دو متدی که در قسمت قبل توضیح داده شد) به صورت مستقیم از کلاس ResourceManager استفاده می‌کند، که برای بحث سفارشی سازی پرووایدرهای منابع مشکل‌زاست. در مطالب بعدی با معایب آن و نیز راه حل‌های موجود آشنا خواهیم شد.

نکات نهایی
حال که با مفاهیم کلی بیشتری آشنا شدیم بهتر است کمی هم به نکات ریزتر بپردازیم:

نکته: فایل تولیدی توسط ویژوال استودیو در فرایند مدیریت منابع ASP.NET تاثیرگذار نیست! باز هم تاکید می‌کنم که کار استخراج کلیدهای Resource از درون فایلهای resx. کاملا به صورت جداگانه و خودکار و در زمان اجرا انجام می‌شود (درباره این فرایند در مطالب بعدی شرح مفصلی خواهد آمد). درواقع شما می‌توانید خاصیت Custom Tool مربوط به منابع کلی را نیز همانند منابع محلی به رشته‌ای خالی مقداردهی کنید و ببینید که خللی در فرایند مربوطه رخ نخواهد داد!

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

نکته: اگر فایل‌های Resource درون اسمبلی‌های جداگانه‌ای باشند (مثلا در یک پروژه جداگانه، همانطور که در قسمت اول این سری مطالب پیشنهاد شده است)، موتور پیش فرض منابع در ASP.NET بدرد نخواهد خورد! بنابراین یا باید از نمونه‌های اختصاصی کلاس ResourceManager استفاده کرد (کاری که کلاس‌های خودکار تولیدشده توسط ابزارهای ویژوال استودیو انجام می‌دهند)، یا باید از پرووایدرهای سفارشی استفاده کرد که در مطالب بعدی نحوه تولید آن‌ها شرح داده خواهد شد.
 
همانطور که در ابتدای این مطلب اشاره شد، این مقدمه در اینجا صرفا برای آشنایی بیشتر با این دونوع Resource آورده شده تا ادامه مطلب روشن‌تر باشد، زیرا با توجه به مطالب ارائه شده در قسمت اول این سری، در پروژه‌های MVC استفاده از یک پروژه جداگانه برای نگهداری این منابع راه حل مناسبتری است.
در مطلب بعدی به شرح نحوه تولید پرووایدرهای سفارشی می‌پردازم.