مطالب
محدود کردن کاربر‌ها به آپلود فایل‌هایی خاص در ASP.NET MVC
یک ادیتور آنلاین را تصور کنید که کاربران در قسمت ارسال تصویر آن قرار است فقط فایل‌های png، jpg و gif ارسال کنند و نه مثلا فایل test.aspx و موارد مشابه. در اینجا برای محدود کردن نوع فایل‌های آپلود شده می‌توان از فیلترهای سفارشی ASP.NET MVC کمک گرفت:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Mvc;

namespace SecurityModule
{
    public class AllowUploadSpecialFilesOnlyAttribute : ActionFilterAttribute
    {
        readonly List<string> _toFilter = new List<string>();
        readonly string _extensionsWhiteList;
        public AllowUploadSpecialFilesOnlyAttribute(string extensionsWhiteList)
        {
            if (string.IsNullOrWhiteSpace(extensionsWhiteList))
                throw new ArgumentNullException("extensionsWhiteList");

            _extensionsWhiteList = extensionsWhiteList;
            var extensions = extensionsWhiteList.Split(',');
            foreach (var ext in extensions.Where(ext => !string.IsNullOrWhiteSpace(ext)))
            {
                _toFilter.Add(ext.ToLowerInvariant().Trim());
            }
        }

        bool canUpload(string fileName)
        {
            if (string.IsNullOrWhiteSpace(fileName)) return false;

            var ext = Path.GetExtension(fileName.ToLowerInvariant());
            return _toFilter.Contains(ext);
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var files = filterContext.HttpContext.Request.Files;
            foreach (string file in files)
            {
                var postedFile = files[file];
                if (postedFile == null || postedFile.ContentLength == 0) continue;

                if (!canUpload(postedFile.FileName))
                    throw new InvalidOperationException(
                        string.Format("You are not allowed to upload {0} file. Please upload only these files: {1}.",
                                        Path.GetFileName(postedFile.FileName), 
                                        _extensionsWhiteList));
            }

            base.OnActionExecuting(filterContext);
        }
    }
}

توضیحات کدهای فوق:
برای تهیه فیلتر محدود سازی نوع فایل‌های قابل ارسال به سرور، با ارث بری از ActionFilterAttribute شروع خواهیم کرد. سپس با تحریف متد OnActionExecuting آن، توسط filterContext.HttpContext.Request.Files می‌توان به کلیه فایل‌های درحال ارسال به سرور در طی درخواست جاری، دسترسی یافت.
به این ترتیب از طریق مقدار خاصیت postedFile.FileName می‌توان به پسوند فایل در حال ارسال رسید و بر این اساس امکان ارسال فایل‌های غیرمجاز را در نیمه راه با صدور یک استثناء سلب کرد.

برای استفاده از این فیلتر سفارشی تهیه شده نیز می‌توان به نحو زیر عمل کرد:
[AllowUploadSpecialFilesOnly(".jpg,.gif,.png")]
public ActionResult ImageUpload(HttpPostedFileBase file)
در اینجا پسوند فایل‌های مجاز قابل ارسال، توسط یک کاما از هم جدا خواهند شد.


یک نکته تکمیلی:
اگر کاربر قرار است تنها تصویر ارسال کند، بررسی پسوند فایل لازم است اما کافی نیست. برای این منظور می‌توان از کلاس Image واقع شده در فضای نام System.Drawing نیز کمک گرفت:
public static bool IsImageFile(HttpPostedFileBase photoFile)
{
    using (var img = Image.FromStream(photoFile.InputStream))
    {
         return img.Width > 0;
    }
}
در اینجا اگر فایل ارسالی تصویر نباشد، به صورت خودکار یک استثناء صادر خواهد شد.
 
مطالب
پیاده سازی حذف منطقی در Entity framework
یکی از روش‌هایی که در اکثر پروژه‌های بزرگ استفاده می‌شود، بحث استفاده از حذف منطقی (soft delete) بجای حذف فیزیکی رکورد می‌باشد (اکثرا در برنامه‌هایی که با بخش مالی (پول) در ارتباط هستند) و از آنجاییکه هیچ برنامه‌ای بدون باگ نمی‌باشد، حذف منطقی بجای حذف فیزیکی پیشنهاد می‌شود. در واقع داشتن و حفظ دیتا، یک امتیاز مثبت می‌باشد؛ به علاوه استرس از دست دادن داده به صورت اتفاقی (سهل انگاری کاربر) را هم نخواهیم داشت. لازم به ذکر است کاربران نهایی استفاده کننده از نرم افزار، خبری از نوع حذف منطقی ندارند.

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

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

برای اینکه بخواهیم حذف منطقی را پیاده سازی نماییم، نیاز داریم به هر رکورد، فیلدی اضافه شود تا از طریق آن مشخص نماییم که آیا رکورد حذف شده است یا خیر. برای این منظور باید فیلدی از نوع boolean به تمام کلاس‌ها (جداول) اضافه شود. می‌توانیم این فیلد را به صورت زیر تعریف کنیم:
  public bool IsDeleted { get; set; }
برای جلوگیری از تکرار کد فوق پیشنهاد میکنم اقدام به ایجاد یک اینترفیس به صورت زیر نمایید:
public interface ISoftDelete
{
    bool IsDeleted { get; set; }
}
و در کلاسی که قصد حذف منطقی را در آن دارید، فقط کافیست از اینترفیس فوق ارث بری نماید:
public class Post :ISoftDelete
{
}


بعد از افزودن فیلد فوق، نیاز داریم تا در تمام کوئری‌ها شرطی را اضافه نماییم تا فقط رکوردهایی را از دیتابیس واکشی کند که حذف نشده‌اند. یعنی فیلد فوق برابر False باشد. در ادامه روش‌هایی برای این هدف بیان خواهند شد.

  روش هایی برای فیلتر رکورد‌های حذف شده
1- افزودن فیلتر زیر در تمامی کوئری‌ها:
where (IsDeleted=false && ...)
در روش فوق نیاز است در تمامی کوئری هایمان شرط فوق را اضافه کنیم. همانطور که حدس زده‌‌اید، در این روش احتمال فراموش شدن شرط فوق وجود دارد و از طرفی یک کد را در همه کوئری‌ها تکرار کرده‌ایم.

2- نوشتن یک متد الحاقی‌:
برای جلوگیری از تکرار شرط فوق می‌توان یک متد الحاقی را به صورت زیر پیاده سازی نمود و در تمامی شرط‌ها، آن را فراخوانی کرد:
public static class EntityFrameworkExtentions
{
    public static ObservableCollection<TEntity> Alive<TEntity>(this DbSet<TEntity> set)
        where TEntity : class, ISoftDelete
    {
        var data = set.Where(e => e.IsDeleted == false);
        return new ObservableCollection<TEntity>(data);
    }
}

3-استفاده از کتابخانه  EntityFramework.DynamicFilte
ابتدا اقدام به نصب بسته آن نمایید:
Install-Package EntityFramework.DynamicFilters
و در کلاس Context خود فیلتر  زیر را قرار دهید :
modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);


مشکلاتی پیرامون حذف منطقی

- کلاس User و Post را در نظر بگیرد که یک User چندین Post دارد. حال اگر حذف، فیزیکی باشد و کاربر اقدام به حذف User مورد نظر کند، با خطای زیر مواجه می‌شود:
The DELETE statement conflicted with the REFERENCE constraint ....
  اما در حذف منطقی چطور؟ در حذف منطقی چون رکوردی حذف نمی‌شود و فقط فیلد IsDeleted به روز رسانی می‌شود، خطای فوق رخ نخواهد داد و همین مورد باعث بروز مشکل می‌شود؛ چون رکوردی که دارای کلید اصلی بوده حذف شده، ولی رکورد‌های وابسته به آن هنوز داخل سیستم موجود می‌باشند. پس برای این مورد نیاز هست ابتدا تمامی Navigation property‌های رکورد مورد نظر یافت شوند و در صورتیکه مقداری وجود نداشت، رکورد مورد نظر حذف فیزیکی شود. برای این منظور دو راه حل پیشنهاد می‌شود:

1- در سرویس‌های مربوط به کلاس‌هایی که از ISoftDelet ارث بری کرده‌اند، متدی تحت عنوان CanDelete، به صورت زیر تعریف شود:
public bool CanDelete(user model)
{
return !model.posts.Any() &&  ! model.news.Any();
}
در متد Candelete، خصوصیات مورد نیاز کلاس را بررسی کرده و در صورتیکه هیچ رکوردی وجود نداشت، کاربر می‌تواند رکورد مورد نظر را حذف نماید.

2- برای جلوگیری از تکرار قطعه کد فوق، میتوان از روش زیر استفاده کرد:
- یک Attribute سفارشی را به صورت زیر تعریف نمایید:
[AttributeUsage(AttributeTargets.Property)]
public class MustBeEmptyToDeleteAttribute : Attribute { }
- به تمامی خصوصیات مورد نیاز که قصد بررسی آنها را داریم، آن‌را به صورت زیر اضافه میکنیم:
public class User
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }

    [MustBeEmptyToDelete] public virtual ICollection<Post> Posts { get; set; }
    [MustBeEmptyToDelete] public virtual ICollection<File> Files { get; set; }
    // etc...
}
و در پایان متد الحاقی زیر، برای بررسی رکورد‌های وابسته می‌باشد:
public static class EntityExtensions
{
    public static bool CanDelete(this object entity)
    {
        return entity.GetType().GetProperties()
            .Where(x => x.IsDefined(typeof(MustBeEmptyToDeleteAttribute)))
            .Select(x => x.GetValue(entity))
            .OfType<IEnumerable<object>>()
            .All(x => !x.Any());
    }

همانطور که بیان شد، در حذف منطقی فقط رکورد مورد نظر به روز رسانی می‌شود. برای این منظور می‌توان دو متد را همانند زیر در نظر گرفت و هر کدام که مورد نیاز بود، فراخوانی شود:
 public void MarkAsSoftDeleted<TEntity>(TEntity entity) where TEntity : ISoftDelete
        {
            Entry(entity).State = EntityState.Modified;
           // set IsDelete=true 
          // here you can set other logs like who deleted ,when ,...
      }

 public void MarkAsDeleted<TEntity>(TEntity entity) where TEntity : class
    {
            Entry(entity).State = EntityState.Deleted;
    }

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


اما چرا PDF؟

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


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


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


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

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

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

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

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

                return Application.StartupPath;
            }
        }

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

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

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

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

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

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

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

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

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


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

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

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

 لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.
مطالب
جایگزین کردن jQuery با JavaScript خالص - قسمت پنجم - درخواست‌های Ajax
AJAX و یا «Asynchronous JavaScript and XML» قابلیتی است که توسط web API جاوا اسکریپتی مرورگرها برای دریافت و یا به روز رسانی اطلاعات، بدون بارگذاری مجدد و کامل صفحه، ارائه می‌شود. این قابلیت اولین بار در سال 1999 توسط مایکروسافت با ارائه‌ی مرورگر IE 5.0 و معرفی شیء XMLHTTP که توسط یک ActiveX control ارائه می‌شد، میسر گردید و این روزها توسط استاندارد XMLHttpRequest در تمام مرورگرها قابل استفاده است. این استاندارد نیز مدتی است که توسط Fetch API مرورگرهای جدید جایگزین شده‌است (پس از 15 سال از ارائه‌ی استاندارد XMLHttpRequest) و در این لحظه در تمام مرورگرهای غالب وب پشتیبانی می‌شود.
در ادامه روش‌های مختلف ارسال درخواست‌های Ajax را توسط jQuery و همچنین معادل‌های XMLHttpRequest و Fetch API آن‌ها بررسی می‌کنیم.


ارسال درخواست‌های GET

توسط استاندارد جدید Fetch API 
  توسط XMLHttpRequest استاندارد  توسط jQuery 
 fetch('/my/name').then(function(response) {
  if (response.ok) {
   return response.text();
  }
  else {
   throw new Error();
  }
 }).then(
  function success(name) {
  console.log('my name is ' + name);
  },
  function failure() {
  console.error('Name request failed!');
  }
 );
 var xhr = new XMLHttpRequest();
  xhr.open('GET', '/my/name');
 xhr.onload = function() {
  if (xhr.status >= 400) {
   console.error('Name request failed!');
  }
  else {
   console.log('my name is ' + xhr.responseText);
  }
 };
 xhr.onerror = function() {
  console.error('Name request failed!');
 };
 xhr.send();
 $.get('/my/name').then(
  function success(name) {
  console.log('my name is ' + name);
  },
  function failure() {
  console.error('Name request failed!');
  }
 );

jQuery برای پشتیبانی از درخواست‌های Ajax، متد ویژه‌ای را به نام ()ajax ارائه می‌کند که برای ارسال درخواست‌هایی از هر نوع، مانندGET، POST و غیره کاربرد دارد. همچنین برای بعضی از نوع‌ها، متدهای کوتاه‌تری را مانند ()get و ()post نیز در اختیار برنامه نویس قرار می‌دهد. جاوا اسکریپت خالص و Web API مرورگرها نیز دو شیء XMLHttpRequest و fetch را برای ارسال درخواست‌های غیرهمزمان، ارائه می‌کند. XMLHttpRequest در تمام مرورگرهای قدیمی و جدید پشتیبانی می‌شود، اما fetch API مدتی است که در غالب مرورگرهای امروزی در دسترس است. در جدول فوق روش ارسال درخواست‌های Ajax از نوع GET را توسط این سه روش مشاهده می‌کنید.
در این مثال‌ها درخواستی به آدرس my/name سمت سرور ارسال شده و انتظار می‌رود که یک plaintext حاوی متن کاربر بازگشت داده شود که در نهایت در console لاگ می‌شود.
- در حالت استفاده‌ی از jQuery در صورت بازگشت موفقیت آمیز پاسخی از طرف سرور، متد success و در غیراینصورت متد failure اجرا می‌شود. باید درنظر داشت که متد ajax جی‌کوئری، چیزی بیشتر از یک محصور کننده‌ی اشیاء XMLHttpRequest نیست.
- در حالت کار با XMLHttpRequest باید اندکی بیشتر تایپ کرد؛ اما اصول کار یکی است. در اینجا onload زمانی فراخوانی می‌شود که پاسخی از سرور دریافت شده و عملیات خاتمه یافته‌است؛ هرچند این پاسخ می‌تواند یک خطا نیز باشد. به همین جهت باید status آن‌را بررسی کرد. اگر رخ‌داد onerror فراخوانی شد، یعنی درخواست، در سطوح بسیار پایین آن مانند بروز یک خطای CORS با شکست مواجه شده‌است.
همانطور که مشاهده می‌کنید در حالت کار با XMLHttpRequest جاوا اسکریپت از اشیاء Promise پشتیبانی نمی‌کند که این کمبود با معرفی fetch API برطرف شده‌است که نمونه‌ای از آن‌را با متد then متصل به fetch مشاهده می‌کنید؛ دقیقا مشابه متد ajax جی‌کوئری که آن نیز یک Promise را بازگشت می‌دهد.
تفاوت Promise جی‌کوئری با fetch API در این است که جی‌کوئری در صورتیکه یک status code بیانگر خطا را دریافت کند، قسمت failure را اجرا می‌کند؛ اما fetch API مانند اشیاء XMLHttpRequest تنها در صورت بروز خطاهای سطح پایین درخواست، این متد را فراخوانی خواهد کرد. هرچند اگر در اینجا response.ok نبود، می‌توان با صدور یک استثناء به رفتاری شبیه به jQuery رسید و قسمت then failure را به صورت خودکار اجرا کرد.


ارسال درخواست‌های Ajax از نوع POST ، PUT و DELETE

در اینجا اطلاعاتی با MIME type از نوع plaintext به سمت سرور ارسال می‌شود. جهت سهولت توضیح و تمرکز بر روی قسمت‌های مهم آن، بخش مدیریت پاسخ آن حذف شده‌است و این مورد دقیقا با مثال قبلی که در مورد درخواست‌های از نوع GET بود، یکی است.
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
توسط jQuery 
 fetch('/user/name', {
  method: 'POST',
  body: 'some data'
 });
 var xhr = new XMLHttpRequest();
 xhr.open('POST', '/user/name');
 xhr.send('some data');
 $.ajax({
  method: 'POST',
  url: '/user/name',
  contentType: 'text/plain',
  data: 'some data'
 });
در حالت ارسال اطلاعات به سرور با متد POST، نیاز است contentType متد ajax جی‌کوئری حتما ذکر شود. در غیراینصورت آن‌را به application/x-www-form-urlencoded تنظیم می‌کند که ممکن است الزاما مقداری نباشد که مدنظر ما است. در اینجا بدنه‌ی درخواست به خاصیت data انتساب داده می‌شود.
اگر از شیء XMLHttpRequest استفاده شود، Content-Type آن به صورت پیش‌فرض به text/plain تنظیم شده‌است. در اینجا بدنه‌ی درخواست به متد send ارسال می‌شود.
متد fetch نیز همانند شیء XMLHttpRequest دارای Content-Type پیش‌فرض از نوع text/plain است. در اینجا بدنه‌ی درخواست به خاصیت body انتساب داده می‌شود.

درخواست‌های از نوع POST عموما برای ایجاد رکوردی جدید در سمت سرور مورد استفاده قرار می‌گیرند و از درخواست‌های PUT بیشتر برای به روز رسانی مقادیر موجود یک رکورد کمک گرفته می‌شود. درخواست‌های از نوع PUT نیز دقیقا مانند درخواست‌های از نوع POST در اینجا مدیریت می‌شوند و در هر سه حالت، متد ارسال اطلاعات، به مقدار PUT تنظیم خواهد شد:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 توسط jQuery 
 fetch('/user/1', {
  method: 'PUT',
  body: //record including new mobile number 
 });
 var xhr = new XMLHttpRequest();
 xhr.open('PUT', '/user/1');
 xhr.send(/* record including new data */);
 $.ajax({
  method: 'PUT',
  url: '/user/1',
  contentType: 'text/plain',
  data: //record including new data
 });
درخواست‌های از نوع DELETE نیز مانند قبل بوده و تنها تفاوت آن، نداشتن بدنه‌ی درخواست است:
 توسط استاندارد جدید Fetch API   توسط XMLHttpRequest استاندارد  توسط jQuery 
 fetch('/user/1', {method: 'DELETE'});
 var xhr = new XMLHttpRequest();
 xhr.open('DELETE', '/user/1');
 xhr.send();
 $.ajax('/user/1', {method: 'DELETE'});


مدیریت Encoding درخواست‌های Ajax

در مثال‌های قبل، اطلاعاتی از نوع text/plain را به سمت سرور ارسال کردیم که به آن encoding type نیز گفته می‌شود. برای تکمیل بحث می‌توان حالت‌های دیگری مانند application/x-www-form-urlencoded، application/json و یا multipart/form-data را که در برنامه‌های کاربردی زیاد مورد استفاده قرار می‌گیرند، بررسی کرد.

کار با URL Encoding

عموما URL encoding در دو قسمت آدرس درخواستی به سرور و یا حتی بدنه‌ی درخواست ارسالی تنظیم می‌شود. MIME type آن نیز application/x-www-form-urlencoded است و اطلاعات آن شامل یکسری جفت کلید/مقدار است. برای متدهای ارسال از نوع GET و DELETE، اطلاعات آن در انتهای آدرس درخواستی و برای سایر حالات در بدنه‌ی درخواست ذکر می‌شوند.
در جی‌کوئری با استفاده از متد param آن می‌توان یک شیء جاوا اسکریپتی را به معادل URL-encoded string آن‌ها تبدیل کرد:
$.param({
  key1: 'some value',
  'key 2': 'another value'
});
با این خروجی encode شده:
 key1=some+value&key+2=another+value
البته باید دقت داشت زمانیکه از متد ajax جی‌کوئری استفاده می‌شود، دیگر نیازی به استفاده‌ی مستقیم از متد param نیست:
 $.ajax({
  method: 'POST',
  url: '/user',
  data: {
  name: 'VahidN',
  address: 'Address 1',
  phone: '555-555-5555'
  }
 });
در اینجا یک شیء جاوا اسکریپتی معمولی به خاصیت data آن نسبت داده شده‌است که در پشت صحنه در حین ارسال به سرور، چون Content-Type پیش‌فرض (و ذکر نشده‌ی در اینجا) دقیقا همان application/x-www-form-urlencoded است، به صورت خودکار تبدیل به یک URL-encoded string می‌شود.

برخلاف جی‌کوئری در حین کار با روش‌های جاوا اسکریپتی خالص این encoding باید به صورت دستی و صریحی انجام شود. برای این منظور دو متد استاندارد encodeURI و encodeURIComponent در جاوا اسکریپت مورد استفاده قرار می‌گیرند. هدف متد encodeURI اعمال آن بر روی یک URL کامل است و یا کلید/مقدارهای جدا شده‌ی توسط & مانند first=Vahid&last=N. اما متد encodeURIComponent صرفا جهت اعمال بر روی یک تک مقدار طراحی شده‌است.
به این ترتیب معادل جاوا اسکریپتی قطعه کد جی‌کوئری فوق به صورت زیر است:
 var xhr = new XMLHttpRequest();
 var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555');
 xhr.open('POST', '/user');
 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
 xhr.send(data);
که در آن data توسط encodeURI تبدیل به یک رشته‌ی URL-encoded شده و سپس با ذکر صریح Content-Type به سمت سرور ارسال می‌شود.
روش انجام اینکار توسط fetch API به صورت زیر است:
   var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555');
  fetch('/user', {
  method: 'POST',
  headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  body: data
 });
معادل متد param جی‌کوئری با جاوا اسکریپت خالص به صورت زیر است؛ برای تبدیل یک شیء جاوا اسکریپتی به معادل URL-encoded string آن:
function param(object) {
    var encodedString = '';
    for (var prop in object) {
        if (object.hasOwnProperty(prop)) {
            if (encodedString.length > 0) {
                encodedString += '&';
            }
            encodedString += encodeURI(prop + '=' + object[prop]);
        }
    }
    return encodedString;
}


کار با JSON Encoding

در عمل JSON نمایش رشته‌ای یک شیء جاوا اسکریپتی است و هدف آن سهولت نقل و انتقالات این اشیاء به سرور و برعکس است. برخلاف حالت application/x-www-form-urlencoded که اطلاعات آن مسطح است، حالت application/json امکان ارسال اطلاعات سلسله مراتبی را نیز میسر می‌کند (مانند مثال زیر که phone آن دیگر مسطح نیست و خود آن نیز یک شیء جاوا اسکریپتی است).
 در جی‌کوئری برای ارسال اشیاء جاوا اسکریپتی JSON Encoded به سمت سرور از روش زیر استفاده می‌شود:
 $.ajax({
  method: 'POST',
  url: '/user',
  contentType: 'application/json',
  data: JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
   home: '555-555-5555',
   mobile: '444-444-4444'
  }
  });
 });
در اینجا نسبت به مثال قبلی، ذکر Content-Type ضروری بوده و همچنین data نیز باید به صورت دستی encode شود. برای این منظور می‌توان از متد استاندارد JSON.stringify استفاده کرد که از زمان IE 8.0 به بعد در تمام مرورگرها پشتیبانی می‌شود.
پیاده سازی همین مثال با جاوا اسکریپت خالص و XMLHttpRequest استاندارد به صورت زیر است:
  var xhr = new XMLHttpRequest();
 var data = JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
    home: '555-555-5555',
    mobile: '444-444-4444'
   }
   });
 xhr.open('POST', '/user');
 xhr.setRequestHeader('Content-Type', 'application/json');
 xhr.send(data);
که در اینجا نیز Content-Type به صورت صریحی ذکر و از متد JSON.stringify برای encode دستی اطلاعات کمک گرفته شده‌است.
در این حالت اگر خروجی سرور نیز JSON باشد، روش دریافت و پردازش آن به صورت زیر است:
 var xhr = new XMLHttpRequest();
 xhr.open('GET', '/user/1');
 xhr.onload = function() {
  var user = JSON.parse(xhr.responseText);
  // do something with this user JavaScript object 
 };
 xhr.send();
رخ‌داد onload، پس از پایان درخواست فراخوانی می‌شود. در اینجا برای دسترسی به response body می‌توان از خاصیت responseText استفاده کرد و سپس توسط متد JSON.parse این رشته را تبدیل به یک شیء جاوا اسکریپتی نمود.
اگر از مرورگر IE صرفنظر کنیم، تمام مرورگرهای دیگر دارای خاصیتی به نام xhr.response نیز هستند که نیاز به تبدیل و Parse دستی رشته‌ی دریافتی را حذف می‌کند؛ از این جهت که این خاصیت حاوی شیء جاوا اسکریپتی معادل بدنه‌ی response دریافتی از سمت سرور است. البته با این شرط که سرور، Content-Type مساوی application/json را برای response تنظیم کرده باشد.
و روش انجام این عملیات توسط fetch API به صورت زیر است:
 fetch('/user', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
   home: '555-555-5555',
   mobile: '444-444-4444'
  }
  });
 });
که در اینجا نیز هدر Content-Type تنظیم و همچنین از متد JSON.stringify برای تبدیل شیء جاوا اسکریپتی به رشته‌ی معادل، استفاده شده‌است.
و یا اگر بخواهیم اطلاعات JSON دریافتی از سمت سرور را در اینجا پردازش کنیم، روش کار به صورت زیر است:
 fetch('/user/1').then(function(response) {
  return response.json();
  }).then(function(userRecord) {
   // do something with this user JavaScript object 
 });
Fetch API به صورت یک Promise، امکان دسترسی به شیء response را مهیا می‌کند. چون می‌دانیم خروجی آن json است، از متد ()json آن که یک Promise را بازگشت می‌دهد استفاده خواهیم کرد. پس از پایان موفقیت آمیز درخواست، then دوم تعریف شده، اجرا و userRecord ارسالی به آن، همان شیء جاوا اسکریپتی دریافتی از سمت سرور است.
همین مثال را اگر بخواهیم توسط ECMAScript 2016 و Arrow functions آن بازنویسی کنیم، به قطعه کد زیر می‌رسیم:
 fetch('/user/1')
  .then(response => response.json()) // Transform the data into json 
  .then(userRecord => {
  // do something with this userRecord object
  });


کار با Multipart Encoding

نوع دیگری از encoding که بیشتر با فرم‌های HTML بکار می‌رود، multipart/form-data نام دارد:
 <form action="my/server" method="POST" enctype="multipart/form-data">
  <label>First Name:
   <input name="first">
  </label>
 
  <label>Last Name:
   <input name="last">
  </label>
 
  <button>Submit</button>
 </form>
با فعال بودن این نوع encoding، اطلاعات نمونه‌ی فرم فوق به شکل زیر به سمت سرور ارسال می‌شوند:
 -----------------------------1686536745986416462127721994
 Content-Disposition: form-data; name="first"

 Vahid
 -----------------------------1686536745986416462127721994
 Content-Disposition: form-data; name="last"

 N
 -----------------------------1686536745986416462127721994--
از این روش نه فقط برای ارسال اطلاعات کلید/مقدارها و اشیاء جاوا اسکریپتی استفاده می‌شود، بلکه از آن برای ارسال اطلاعات فایل‌های باینری نیز کمک گرفته می‌شود.
روش ارسال اطلاعات با این نوع encoding خاص به سمت سرور توسط متد ajax جی‌کوئری به صورت زیر است:
  var formData = new FormData();
 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');
 
 $.ajax({
  method: 'POST',
  url: '/user',
  contentType: false,
  processData: false,
  data: formData
 });
همانطور که ملاحظه می‌کنید jQuery روش توکاری را برای انجام اینکار نداشته و باید از FormData جاوا اسکریپت یا همان web API مرورگرها به همراه متد ajax آن استفاده کرد. در این حالت اطلاعات به صورت کلید/مقدارها به شیء استاندارد FormData اضافه شده و سپس به سمت سرور ارسال می‌شوند. باید دقت داشت FormData از نگارش‌های پس از IE 9.0 در دسترس است.
در اینجا ذکر processData: false ضروری است. در غیراینصورت jQuery این اطلاعات را به یک URL-encoded string تبدیل می‌کند. همچنین با اعلام contentType: false، جی‌کوئری در کار مرورگر دخالت نمی‌کند. از این جهت که هدر ویژه‌ی این نوع درخواست‌ها توسط خود مرورگر تنظیم می‌شود و برای مثال یک چنین شکلی را دارد:
 multipart/form-data; boundary=—————————1686536745986416462127721994
این عدد انتهای هدر یک unique ID است که جزئی از اطلاعات multipart بوده و توسط مرورگر به انتهای هدر اصلی multipart/form-data اضافه می‌شود.

روش انجام اینکار با XMLHttpRequest و همچنین fetch API به صورت زیر است:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 var formData = new FormData();
 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');

 fetch('/user', {
  method: 'POST',
  body: formData
 });
 var formData = new FormData(),
  xhr = new XMLHttpRequest();

 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');

 xhr.open('POST', '/user');
 xhr.send(formData);
همانطور که مشاهده می‌کنید قسمت استفاده‌ی از FormData استاندارد در اینجا یکسان است و همچنین نیازی به ذکر هدر و یا اطلاعات اضافه‌تری نیست.


آپلود فایل‌ها توسط درخواست‌های Ajax ایی

تنها راه آپلود فایل‌ها در مرورگرهای قدیمی که شامل IE 9.0 هم می‌شود، تعریف المان <"input type="file> در داخل المان <form> و سپس submit مستقیم آن فرم است. برای رفع این مشکل در مرورگرهای پس از IE 9.0 و پشتیبانی از Ajax، جهت آپلود فایل‌ها، استاندارد XMLHttpRequest Level 2 معرفی شده‌است. در این حالت اگر المان <input type=file> در صفحه وجود داشته باشد، روش ارسال Ajax ایی آن به سمت سرور به صورت زیر است:
توسط استاندارد جدید Fetch API 
 توسط XMLHttpRequest استاندارد  توسط jQuery 
var file = document.querySelector(
      'INPUT[type="file"]').files[0];
fetch('/uploads', {
  method: 'POST',
  body: file
  });
 }
var file = document.querySelector(
    'INPUT[type="file"]').files[0],
xhr = new XMLHttpRequest();
xhr.open('POST', '/uploads');
xhr.send(file);

var file = $('INPUT[type="file"]')[0].files[0]; 
$.ajax({
   method: 'POST',
   url: '/uploads',
   contentType: false,
   processData: false,
   data: file
  });
در اینجا روش آپلود یک تک فایل را به سرور، توسط درخواست‌های Ajax ایی مشاهده می‌کنید و توضیحات contentType: false و processData: false آن مانند قبل است تا jQuery این اطلاعات multipart را پیش از ارسال به سرور دستکاری نکند.
یک نکته: اگر نیاز به آپلود بیش از یک فایل را داشتید و همچنین در اینجا نیاز به اطلاعات دیگری مانند سایر فیلدهای فرم نیز وجود داشت، از همان روش تعریف  new FormData و افزودن اطلاعات مورد نیاز به آن استفاده کنید. امکان افزودن شیء file نیز به FormData پیش بینی شده‌است.


دانلود فایل‌ها توسط درخواست‌های Ajax ایی

پیشتر در حین بررسی JSON encoding توسط fetch API از متد ()json برای تبدیل اطلاعات دریافتی از سرور به json و بازگشت آن به صورت یک Promise استفاده کردیم:
  fetch(url) 
  .then((resp) => resp.json()) // Transform the data into json 
  .then(function(data) { 
    // use data object
    }) 
  })
در اینجا علاوه بر ()json، متدهای استاندارد دیگری نیز پیش بینی شده‌اند که همگی یک Promise را بازگشت می‌دهند:
- ()clone یک کپی از response را تهیه می‌کند.
- ()redirect یک response جدید را با URL دیگری ایجاد می‌کند.
- ()arrayBuffer یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء ArrayBuffer تبدیل می‌کند.
- ()formData یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء FormData تبدیل می‌کند.
- ()blob یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء Blob تبدیل می‌کند.
- ()text یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به string تبدیل می‌کند.
- ()json یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء جاوا اسکریپتی تبدیل می‌کند.

در اینجا متدی که می‌تواند برای تبدیل یک byte array بازگشتی از سرور به فایل قابل دریافت در سمت کلاینت مورد استفاده قرار گیرد، متد blob است:
function downloadBlob()
{
       fetch('/Home/InMemoryReport')
   .then(function(response) {
         return response.blob();
   })
   .then(function(xlsxBlob) {
             var a = document.createElement("a");
             document.body.appendChild(a);
             a.style = "display: none";
             let url = window.URL.createObjectURL(xlsxBlob);
             a.href = url;
             a.download = "report.xlsx";
             a.click();
             window.URL.revokeObjectURL(url);
   });
}
فرض کنید مسیر Home/InMemoryReport یک گزارش PDF و یا اکسل را به صورت byte array بازگشت می‌دهد. اولین then نوشته شده، درخواست تبدیل این byte array را پس از پایان response به یک شیء Blob می‌دهد. پس از پایان درخواست، این Promise اجرا شده و نتیجه‌ی آن به صورت خودکار در اختیار then دوم قرار می‌گیرد. در اینجا همانطور که در قسمت قبل نیز بررسی کردیم، یک المان جدید anchor مخفی را ایجاد کرده و به صفحه اضافه می‌کنیم. سپس url آن‌را به این شیء Blob، توسط متد استاندارد window.URL.createObjectURL تنظیم می‌کنیم. با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. نام این فایل را هم توسط ویژگی download این شیء می‌توان تنظیم نمود. در نهایت بر روی آن، متد click را فراخوانی می‌کنیم. با اینکار مرورگر این فایل را به صورت یک فایل دریافت شده‌ی متداول در لیست فایل‌های آن قرار می‌دهد. این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد.


ارسال درخواست‌های Ajax به دومین‌های دیگر (CORS)

گاهی از اوقات نیاز است اطلاعاتی را توسط درخواست‌های Ajax، به سروری دیگر در دومینی دیگر ارسال و یا دریافت کرد. هرچند انجام اینکار به صورت مستقیم و خارج از مرورگر بدون مشکل قابل انجام است، اما مرورگرها برای درخواست‌های جاوا اسکریپتی محدودیت «same-origin policy» را اعمال می‌کنند. به این معنا که XMLHttpRequest بین دومین‌ها به صورت پیش‌فرض ممنوع است. برای ارسال درخواست‌های مجاز و از پیش مشخص شده‌ی Ajax بین دومین‌ها، تاکنون دو روش پیش بینی شده‌است:
الف) روش JSONP
«same-origin policy» از شروع ارسال درخواستی به خارج از دومین جاری، جلوگیری می‌کند. هرچند این مورد به درخواست‌های XMLHttpRequest اعمال می‌شود، اما در مورد المان‌هایی از نوع <a>، <img>  و  <script> صادق نیست و آن‌ها محدود به این سیاست امنیتی نیستند. روش «JavaScript Object Notation with Padding» و یا به اختصار JSONP از یکی از همین استثناءها جهت ارسال درخواست‌هایی به سایر دومین‌ها استفاده می‌کند. البته نام این روش کمی غلط انداز است؛ از این جهت که در این فرآیند اصلا JSON ایی مورد استفاده قرار نمی‌گیرد؛ خروجی سرور در این حالت یک تابع جاوا اسکریپتی است و نه JSON.
روش انجام این نوع درخواست‌ها را توسط جی‌کوئری در ذیل مشاهده می‌کنید:
 $.ajax('http://jsonp-aware-endpoint.com/user/1', {
  jsonp: 'callback',
  dataType: 'jsonp'
 }).then(function(response) {
  // handle user info from server 
 });
در این حالت jQuery پس از اجرای تابع دریافتی از سرور، نتیجه‌ی آن‌را در قسمت then، در اختیار مصرف کننده قرار می‌دهد.
انجام اینکار بدون jQuery و در حقیقت کاری که jQuery در پشت صحنه برای ایجاد تگ script انجام می‌دهد، چنین چیزی است:
 window.myJsonpCallback = function(data) {
  // handle user info from server 
 };

 var scriptEl = document.createElement('script');
 scriptEl.setAttribute('src',
  'http://jsonp-aware-endpoint.com/user/1?callback=myJsonpCallback');
 document.body.appendChild(scriptEl);
هرچند این روش هنوز هم در بعضی از سایت‌ها مورد استفاده‌است، در کل بهتر است از آن دوری کنید؛ چون ممکن است به مشکلات امنیتی دیگری ختم شود. این روش بیشتر در روزهای آغازین معرفی محدودیت «same-origin policy» بکار گرفته می‌شد (در زمان IE 7.0).


ب) روش CORS

CORS  و یا Cross Origin Resource Sharing روش مدرن و پذیرفته شده‌ی ارسال درخواست‌های Ajax در بین دومین‌ها است و دارای دو نوع ساده و غیرساده است. نوع ساده‌ی آن به همراه هدر مخصوص Origin است که جهت بیان دومین ارسال کننده‌ی درخواست بکار می‌رود و تنها از encodingهای “text/plain”  و  “application/x-www-form-urlencoded”  پشتیبانی می‌کند. نوع غیرساده‌ی آن که این روزها بیشتر بکار می‌رود، از نوع «preflight» است. Preflight در اینجا به این معنا است که زمانیکه درخواست Ajax ایی را به دومین دیگری ارسال کردید، پیش از ارسال، مرورگر یک درخواست از نوع OPTIONS را به سمت سرور مقصد ارسال می‌کند. در این حالت اگر سرور مجوز مناسبی را صادر کرد، آنگاه مرورگر اصل درخواست را به سمت آن سرور ارسال می‌کند. به همین جهت در این حالت به ازای هر درخواستی که در برنامه ارسال می‌شود، در برگه‌ی network مرورگر، دو درخواست را مشاهده خواهید کرد. درخواست preflight از نوع OPTIONS به صورت خودکار توسط مرورگر مدیریت می‌شود و نیازی به کدنویسی خاصی ندارد.


مدیریت کوکی‌ها در درخواست‌های Ajax

اگر درخواست Ajax ایی را به دومین دیگری ارسال کنید، به صورت پیش‌فرض به همراه کوکی‌های مرتبط نخواهد بود. برای رفع این مشکل نیاز است خاصیت withCredentials را به true تنظیم کنید:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 توسط jQuery 
 fetch('http://someotherdomain.com', {
  method: 'POST',
  headers: {
  'Content-Type': 'text/plain'
  },
  credentials: 'include'
 });
 var xhr = new XMLHttpRequest();
 xhr.open('POST', 'http://someotherdomain.com');
 xhr.withCredentials = true;
 xhr.setRequestHeader('Content-Type', 'text/plain');
 xhr.send('sometext');
 $.ajax('http://someotherdomain.com', {
  method: 'POST',
  contentType: 'text/plain',
  data: 'sometext',
  beforeSend: function(xmlHttpRequest) {
  xmlHttpRequest.withCredentials = true;
  }
 });
 
یک نکته‌ی مهم: در fetch API حتی برای درخواست‌های ساده نیز کوکی‌ها ارسال نمی‌شوند. در این حالت برای کار با دومین جاری و ارسال کوکی‌های کاربر به سمت سرور، باید از تنظیم 'credentials: 'same-origin استفاده کرد؛ زیرا مقدار پیش‌فرض آن omit است.
مطالب
پیاده سازی RabbitMQ
RabbitMq شبیه به یک صف FIFO عمل میکند؛ یعنی داده‌ها به ترتیب وارد queue میشوند و به ترتیب نیز به Consumer‌ها ارسال میشوند. برای شروع، یک سولوشن جدید را به نام RabbitMqExample ایجاد میکنیم و پروژه‌های زیر را به آن اضافه میکنیم.
  • یک پروژه از نوع Asp.Net Core Web Application ایجاد میکنیم به نام RabbiMqExample.Producer که همان ارسال کننده (Producer) میباشد.
  • یک پروژه از نوع Asp.Net Core Web Application به نام RabbitMqExample.Consumer برای دریافت کننده (Consumer).
  • یک پروژه از نوع Class library .Net Core به نام RabbitMqExample.Common که شامل سرویس‌ها و مدل‌های مشترک بین Producer و Consumer میباشد.
ابتدا در لایه Common یک کلاس برای دریافت اطلاعات RabbitMq از appsettings.json ایجاد میکنیم.
public class RabbitMqConfiguration
{
    public string HostName { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}
سپس یک سرویس را برای برقراری ارتباط با RabbitMq ایجاد میکنیم
public interface IRabbitMqService
{
    IConnection CreateChannel();
}

public class RabbitMqService : IRabbitMqService
{
    private readonly RabbitMqConfiguration _configuration;
    public RabbitMqService(IOptions<RabbitMqConfiguration> options)
    {
        _configuration = options.Value;
    }
    public IConnection CreateChannel()
    {
        ConnectionFactory connection = new ConnectionFactory()
        {
            UserName = _configuration.Username,
            Password = _configuration.Password,
            HostName = _configuration.HostName
        };
        connection.DispatchConsumersAsync = true;
        var channel = connection.CreateConnection();
        return channel;
    }
}
در متد CreateChannel، اطلاعات موردنیاز برای ارتباط با RabbitMq را مانند هاست، نام کاربری و کلمه عبور، وارد میکنیم که از appsettings.json خوانده شده‌اند. مقدار پیش‌فرض نام کاربری و کلمه عبور، guest میباشد.
 اگر بخواهید Consumer شما داده‌های queue‌ها را به صورت async دریافت کند، باید مقدار پراپرتی DispatchConsumersAsync مربوط به ConnectionFactory را برابر true کنید. مقدار پیشفرض آن false است.  
در ادامه یک کلاس را برای رجیستر کردن سرویس‌ها ایجاد میکنیم؛ در لایه Common.
public static class StartupExtension
{
    public static void AddCommonService(this IServiceCollection services, IConfiguration configuration)
    {
        services.Configure<RabbitMqConfiguration>(a => configuration.GetSection(nameof(RabbitMqConfiguration)).Bind(a));
        services.AddSingleton<IRabbitMqService, RabbitMqService>();
    }
}
پکیج‌های مورد نیاز این لایه :
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
    <PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
  </ItemGroup>
تا به اینجا موارد مربوط به لایه Common تمام شده؛ در ادامه باید یک Consumer و یک Producer را ایجاد کنیم.
در لایه Consumer برای دریافت داده‌ها از RabbitMq؛ یک سرویس را به نام ConsumerService ایجاد میکنیم:
public interface IConsumerService
{
    Task ReadMessgaes();
}

public class ConsumerService : IConsumerService, IDisposable
{
    private readonly IModel _model;
    private readonly IConnection _connection;
    public ConsumerService(IRabbitMqService rabbitMqService)
    {
        _connection = rabbitMqService.CreateChannel();
        _model = _connection.CreateModel();
        _model.QueueDeclare(_queueName, durable: true, exclusive: false, autoDelete: false);
        _model.ExchangeDeclare("UserExchange", ExchangeType.Fanout, durable: true, autoDelete: false);
        _model.QueueBind(_queueName, "UserExchange", string.Empty);
    }
    const string _queueName = "User";
    public async Task ReadMessgaes()
    {
        var consumer = new AsyncEventingBasicConsumer(_model);
        consumer.Received += async (ch, ea) =>
        {
            var body = ea.Body.ToArray();
            var text = System.Text.Encoding.UTF8.GetString(body);
            Console.WriteLine(text);
            await Task.CompletedTask;
            _model.BasicAck(ea.DeliveryTag, false);
        };
        _model.BasicConsume(_queueName, false, consumer);
        await Task.CompletedTask;
    }

    public void Dispose()
    {
        if (_model.IsOpen)
            _model.Close();
        if (_connection.IsOpen)
            _connection.Close();
    }
}
ابتدا connection را ایجاد کرده‌ایم؛ توسط متد CreateChannel که آنرا در سرویس قبلی پیاده سازی کردیم.
بعد از ایجاد IModel، باید queue مربوطه را معرفی کنیم که با استفاده از متد QueueDeclare این کار را انجام داده‌ایم. 
پارامترهای متد QueueDeclare:
  • پارامتر اول، اسم queue میباشد 
  • پارامتر durable مشخص میکند که داده‌ها به صورت مانا باشند یا نه. اگر برابر true باشد، دیتاهای مربوط به queue‌ها، در دیسک ذخیره میشوند؛ اما اگر برابر false باشد، بر روی حافظه ذخیره میشوند. در محیط‌هایی که مانایی اطلاعات مهم میباشد، باید مقدار این پارامتر را true کنید.
  • پارامتر سوم: اطلاعات بیشتر
  • پارامتر autoDelete اگر برابر true باشد، زمانی که تمامی Consumer‌ها ارتباطشان با RabbitMq قطع شود، queue هم پاک میشود. اما اگر برابر true باشد، queue باقی میماند؛ حتی اگر هیچ Consumer ای به آن وصل نباشد.
در ادامه باید Exchange مربوط به queue را مشخص کنیم. متد ExchangeDeclare یک Exchange را ایجاد میکند. پارامتر‌های متد ExchangeDeclare:
  • نام Exchange
  • نوع Exchange که میتواند Headers , Topic ,  Fanout یا Direct باشد. اگر برابر Fanout باشد و اگر داده‌ای وارد Exchange شود، آن‌را به تمامی queue هایی که به آن بایند شده‌است، ارسال میکند. اما اگر نوع آن Direct باشد، داده را به یک queue مشخص ارسال میکند؛ با استفاده از پارامتر routeKey.
  • پارامتر‌های بعدی، durable و autoDelete هستند که همانند پارامترهای QueueDeclare عمل میکنند.
سپس در ادامه با استفاده از متد QueueBind میتوانیم queue ایجاد شده را به exchange ایجاد شده، بایند کنیم. پارامتر اول، اسم queue و پارامتر دوم، اسم exchange میباشد و پارامتر سوم، routeKey میباشد و چون نوع Exchange ایجاد شده از نوع Fanout است، آنرا خالی میگذاریم. 

چون هنگام تعریف queue مقدار پارامتر DispatchConsumersAsync مربوط به ConnectionFactory را برابر true کردیم، در اینجا نیز باید بجای EventingBasicConsumer، از AsyncEventingBasicConsumer استفاده کنیم. اگر مقدار DispatchConsumersAsync برابر false باشد، باید از EventingBasicConsumer برای ایجاد Consumer استفاده کنید. 

سپس باید EventHandler مربوط به دریافت داده‌ها از queue را پیاده سازی کنیم. event مربوط به Received، زمانی اجرا میشود که داده‌ای به queue ارسال شود. زمانیکه داده‌ای ارسال میشود، وارد event مربوطه میشود و ابتدا آنرا به صورت byte دریافت میکنیم. سپس رشته‌ی ارسالی آن‌را توسط متد GetString، بدست می‌آوریم و داده‌ی ارسال شده را در صفحه‌ی کنسول نمایش میدهیم.

 در ادامه به RabbitMq اطلاع میدهیم که داده‌ای که ارسال شده برای queue، توسط Consumer دریافت شده؛ با استفاده از متد BasicAck. این کار یک delivery  به RabbitMq ارسال میکند تا دیتای ارسال شده را را پاک کند. اگر این متد را فراخوانی نکنیم، هربار که برنامه اجرا میشود، تمامی دیتاهای قبلی را مجددا دریافت میکنیم و تا زمانیکه delivery را به RabbitMq نفرستیم، داده‌ها را پاک نمیکند.

نکته آخر در Consumer، متد BasicConsume است که عملا Consumer ایجاد شده را به RabbitMq معرفی میکند. برای دریافت داده‌ها و ثبت Consumer، نیازمند آن هستیم تا یکبار متد ReadMessage فراخوانی شود. برای همین یک HostedService ایجاد میکنیم تا یکبار این متد را فراخوانی کند:
public class ConsumerHostedService : BackgroundService
{
    private readonly IConsumerService _consumerService;

    public ConsumerHostedService(IConsumerService consumerService)
    {
        _consumerService = consumerService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await _consumerService.ReadMessgaes();
    }
}
در نهایت سرویس‌های ایجاد شده را رجیستر میکنیم؛ در Startup لایه Consumer
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddCommonService(Configuration);
        services.AddSingleton<IConsumerService, ConsumerService>();
        services.AddHostedService<ConsumerHostedService>();
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    }
}
تا به اینجا کارهای مربوط به Consumer تمام شده و باید قسمت Producer آنرا پیاده سازی کنیم.
در لایه Producer یک کنترلر به نام RabbitController را ایجاد میکنیم که شامل یک متد میباشد که داده‌ها را به Queue ارسال میکند:
[Route("api/[controller]/[action]")]
[ApiController]
public class RabbitController : ControllerBase
{
    private readonly IRabbitMqService _rabbitMqService;

    public HomeController(IRabbitMqService rabbitMqService)
    {
        _rabbitMqService = rabbitMqService;
    }
    [HttpPost]
    public IActionResult SendMessage()
    {
        using var connection = _rabbitMqService.CreateChannel();
        using var model = connection.CreateModel();
        var body = Encoding.UTF8.GetBytes("Hi");
        model.BasicPublish("UserExchange",
                             string.Empty,
                             basicProperties: null,
                             body: body);

        return Ok();
    }
}
در متد SendMessage، ابتدا ارتباط خود را با RabbitMq برقرار میکنیم و سپس دیتای "Hi" را به صورت byte، به RabbitMq ارسال میکنیم؛ توسط متد BasicPublish.
پارامتر اول، اسم Exchange است و پارامتر دوم، routeKey و body هم دیتای ارسالی میباشد. در نهایت سرویس‌های مربوط به لایه Producer را رجیستر میکنیم؛ در Startup لایه Consumer:
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; set; }
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddCommonService(Configuration);
    }
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
        });
    }
}
اکنون اگر هر دو پروژه را اجرا کنید و متد SendMessage مربوط به کنترلر Rabbit را فراخوانی کنید، بعد از آنکه پیام شما ارسال شد، در صفحه کنسول مربوط به Consumer، رشته ارسال شده را مشاهده میکنید.
فایل appsetting.json مربوط به پروژه‌های Consumer و Producer:
{
  "RabbitMqConfiguration": {
    "HostName": "localhost",
    "Username": "guest",
    "Password": "guest"
  }
}
فایل docker-compose.yml برای اجرای RabbitMq بر روی داکر:
version: "3.2"
services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: 'rabbitmq'
    ports:
        - 5672:5672
        - 15672:15672
کدهای این مقاله را میتوانید از گیت‌هاب دانلود کنید.
نظرات مطالب
نوشتن Middleware سفارشی در ASP.NET Core
یک نکته‌ی تکمیلی: انتقال کدهای IHttpModule‌ها به میان افزارها

معادل BeginRequest و EndRequest در ماژول‌های نگارش‌های پیشین ASP.NET:
namespace MyApp.Modules
{
    public class MyModule : IHttpModule
    {
        public void Dispose()
        {
        }

        public void Init(HttpApplication application)
        {
            application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
            application.EndRequest += (new EventHandler(this.Application_EndRequest));
        }

        private void Application_BeginRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the beginning of request processing.
        }

        private void Application_EndRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the end of request processing.
        }
    }
}
دقیقا مکان‌های پیش و پس از فراخوانی await _next در میان افزارها هستند:
public class EndRequestMiddleware
{
    private readonly RequestDelegate _next;

    public EndRequestMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        // Do tasks before other middleware here, aka 'BeginRequest'
        // ...

        // Let the middleware pipeline run
        await _next(context);

        // Do tasks after middleware here, aka 'EndRequest'
        // ...
    }
}
مطالب دوره‌ها
استفاده از Factories برای حذف Service locators در برنامه‌های WinForms
یک برنامه‌ی WinForms را درنظر بگیرید که از دو فرم تشکیل شده است.
فرم اول کار نمایش فرم 2 را به عهده دارد.
فرم دوم کار ارسال ایمیل را انجام می‌دهد. این ایمیل نیز از طریق سرویس ذیل فراهم می‌شود:
namespace WinFormsIoc.Services
{
    public interface IEmailsService
    {
        void SendEmail(string from, string to, string title, string message);
    }
}

namespace WinFormsIoc.Services
{
    public class EmailsService : IEmailsService
    {
        public void SendEmail(string from, string to, string title, string message)
        {
            //todo: ...
        }
    }
}
پیاده سازی متد SendEmail در اینجا مدنظر نیست. نکته‌ی مهم، مدیریت تامین و تزریق وابستگی‌های تعریف شده در سازنده‌ی آن است:
    public partial class Form2 : Form
    {
        private readonly IEmailsService _emailsService;
        public Form2(IEmailsService emailsService)
        {
            _emailsService = emailsService;
            InitializeComponent();
        }
احتمالا شاید عنوان کنید که در فرم اول، زمانیکه نیاز است فرم دوم نمایش داده شود، می‌نویسیم new Form2 و در پارامتر آن با استفاده از متد ObjectFactory.GetInstance سازنده‌ی آن‌را فراهم می‌کنیم:
 var form2 = new Form2(ObjectFactory.GetInstance<IEmailsService>());
form2.Show();
و یا اگر مدتی با IoC Containers کار کرده باشید، شاید پیشنهاد دهید که فقط بنویسید:
 var form2 = ObjectFactory.GetInstance<Form2>();
form2.Show();
و همین! به صورت خودکار اگر n پارامتر تزریق شده هم در سازنده‌ی فرم دوم وجود داشته باشند، بر اساس تنظیمات اولیه‌ی IoC Container مورد استفاده، نمونه سازی شده و برنامه بدون مشکل کار خواهد کرد.

مشکل! این دو راه حل هیچکدام به عنوان تزریق وابستگی‌ها شناخته نمی‌شوند و به الگوی Service locator معروف هستند. مشکل آن‌ها این است که کدهای ما در حال حاضر وابستگی مستقیمی به IoC container مورد استفاده پیدا کرده‌اند. در حالت اول ما خودمان دستی درخواست داده‌ایم که کدام وابستگی باید وهله سازی شود و در حالت دوم همانند حالت اول، کدهای ObjectFactory.GetInstance، مختص به یک IoC Container خاص است. نحوه‌ی صحیح کار با IoC Container‌ها باید به این نحو باشد که یکبار در آغاز برنامه تنظیم شوند و در ادامه سایر کلاس‌های برنامه طوری کار کنند که انگار IoC Container ایی وجود خارجی ندارد.


راه حل: ObjectFactory.GetInstance را کپسوله کنید.

using System.Windows.Forms;

namespace WinFormsIoc.IoC
{
    public interface IFormFactory
    {
        T Create<T>() where T : Form;
    }
}

using System.Windows.Forms;
using StructureMap;

namespace WinFormsIoc.IoC
{
    public class FormFactory : IFormFactory
    {
        public T Create<T>() where T : Form
        {
            return ObjectFactory.GetInstance<T>();
        }
    }
}
در اینجا یک اینترفیس را تعریف کرده‌ایم که متد ایجاد وهله‌ا‌ی از یک فرم را ارائه می‌دهد. پیاده سازی آن در برنامه‌‌ای که از StructureMap استفاده می‌کند، مطابق کلاس FormFactory است. اگر IoC Container دیگری باشد، فقط باید این پیاده سازی را تغییر دهید و نه کل برنامه را. اکنون برای استفاده از آن، IFormFactory را در سازنده‌ی کلاسی که نیاز دارد فرم‌های دیگر را نمایش دهد، تزریق می‌کنیم:
using System;
using System.Windows.Forms;
using WinFormsIoc.IoC;

namespace WinFormsIoc
{
    public partial class Form1 : Form
    {
        private readonly IFormFactory _formFactory;
        public Form1(IFormFactory formFactory)
        {
            _formFactory = formFactory;
            InitializeComponent();
        }

        private void btnShowForm2_Click(object sender, EventArgs e)
        {
            var form2 = _formFactory.Create<Form2>();
            form2.Show();
        }
    }
}
در کدهای فوق، فرم اول برنامه را ملاحظه می‌کنید که قرار است فرم دوم را نمایش دهد. IFormFactory در سازنده‌ی آن تزریق شده‌است. با فراخوانی متد Create آن، فرم دوم برنامه به همراه تمام وابستگی‌های تزریق شده‌ی در سازنده‌ی آن وهله سازی می‌شوند.
نکته‌ی مهم این کدها عدم وابستگی مستقیم آن به هیچ نوع IoC Container خاصی است. این فرم اصلا نمی‌داند که IoC Container ایی در برنامه وجود دارد یا خیر.


مشکل! با تغییر سازنده‌ی Form1 برنامه دیگر کامپایل نمی‌شود!
اگر فایل Program.cs را باز کنید، یک چنین سطری را دارد:
 Application.Run(new Form1());
چون سازنده‌ی فرم یک، اکنون پارامتر جدیدی پیدا کرده‌است، در اینجا می‌توان ObjectFactory.GetInstance را مستقیما بکار برد (در این حالت خاص که مرتبط است به کلاس آغازین برنامه، با توجه به اینکه وهله سازی آن مستقیما و خارج از کنترل ما انجام می‌شود، دیگر چاره‌ای نداریم و مجبور هستیم از الگوی Service locator استفاده کنیم).
 Application.Run(ObjectFactory.GetInstance<Form1>());

مثال کامل این بحث را از اینجا می‌توانید دریافت کنید
WinFormsIoc.zip
بازخوردهای پروژه‌ها
اضافه کردن چند متد الحاقی
با سلام من توی پروژه هام چند تا متد استفاده می‌کنم. خوشحال می‌شم نظر شمارو هم بدونم و به پروژه اتون اضافه اش کنید.

این متد برای محاسبه اختلاف دو تاریخ استفاده میشه، البته منبعش یادم نیست این متد از کجا گرفتم:
/// <summary>
        /// DateDiff in SQL style.
        /// Datepart implemented:
        ///     "year" (abbr. "yy", "yyyy"),
        ///     "quarter" (abbr. "qq", "q"),
        ///     "month" (abbr. "mm", "m"),
        ///     "day" (abbr. "dd", "d"),
        ///     "week" (abbr. "wk", "ww"),
        ///     "hour" (abbr. "hh"),
        ///     "minute" (abbr. "mi", "n"),
        ///     "second" (abbr. "ss", "s"),
        ///     "millisecond" (abbr. "ms").
        /// </summary>
        /// <param name="startDate"></param>
        /// <param name="datePart"></param>
        /// <param name="endDate"></param>
        /// <returns></returns>
        public static Int64 DateDiff(this DateTime startDate, String datePart, DateTime endDate)
        {
            Int64 dateDiffVal;
            System.Globalization.Calendar cal = System.Threading.Thread.CurrentThread.CurrentCulture.Calendar;
            TimeSpan ts = new TimeSpan(endDate.Ticks - startDate.Ticks);
            switch (datePart.ToLower().Trim())
            {
                #region year

                case "year":
                case "yy":
                case "yyyy":
                    dateDiffVal = cal.GetYear(endDate) - cal.GetYear(startDate);
                    break;

                #endregion

                #region quarter

                case "quarter":
                case "qq":
                case "q":
                    dateDiffVal = (((cal.GetYear(endDate) - cal.GetYear(startDate)) * 4) + ((cal.GetMonth(endDate) - 1) / 3)) - ((cal.GetMonth(startDate) - 1) / 3);
                    break;

                #endregion

                #region month

                case "month":
                case "mm":
                case "m":
                    dateDiffVal = ((cal.GetYear(endDate) - cal.GetYear(startDate)) * 12 + cal.GetMonth(endDate)) - cal.GetMonth(startDate);
                    break;

                #endregion

                #region day

                case "day":
                case "d":
                case "dd":
                    dateDiffVal = (Int64)ts.TotalDays;
                    break;

                #endregion

                #region week

                case "week":
                case "wk":
                case "ww":
                    dateDiffVal = (Int64)(ts.TotalDays / 7);
                    break;

                #endregion

                #region hour

                case "hour":
                case "hh":
                    dateDiffVal = (Int64)ts.TotalHours;
                    break;

                #endregion

                #region minute

                case "minute":
                case "mi":
                case "n":
                    dateDiffVal = (Int64)ts.TotalMinutes;
                    break;

                #endregion

                #region second

                case "second":
                case "ss":
                case "s":
                    dateDiffVal = (Int64)ts.TotalSeconds;
                    break;

                #endregion

                #region millisecond

                case "millisecond":
                case "ms":
                    dateDiffVal = (Int64)ts.TotalMilliseconds;
                    break;

                #endregion

                default:
                    throw new Exception(String.Format("DatePart \"{0}\" is unknown", datePart));
            }
            return dateDiffVal;
        }

بررسی اینکه آیا رشته ورودی به الگوی مورد نظر مطابقت دارد یا خیر؟
/// <summary>
        /// بررسی اینکه رشته وارد شده با قالب مورد نظر مطابقت دارد یا خیر
        /// </summary>
        /// <param name="source">متن وارد شده</param>
        /// <param name="regexTemplate">قالب مورد نظر</param>
        /// <returns></returns>
        public static Boolean IsMatchTemplate(this String source, String regexTemplate)
        {
            var regex = new Regex(regexTemplate);
            return regex.IsMatch(source);
        }
البته متدهای زیادی دارم می‌ترسم وجهه خوبی نداشته باشه همش اینجا بذارم، با اجازه مدیریت سایت در صورت تمایل ایمیلتون بهم پیام خصوصی بدین، که متد هارو براتون ارسال کنم.

مطالب
استفاده از SignalR در اندروید
همانطور که مطلع هستید، بخش سورس باز مایکروسافت برای برنامه‌نویس‌های جاوا نیز SDKی جهت استفاده از SignalR ارائه کرده است. در اینجا می‌توانید مخزن کد آن را در گیت‌هاب مشاهده کنید. هنوز مستنداتی برای این SDK به صورت قدم به قدم ارائه نشده است. لازم به ذکر است که مراجعه به قسمت‌های نوشته شده در اینجا نیز می‌تواند منبع خوبی برای شروع باشد. در ادامه نحوه استفاده از این SDK را با هم بررسی خواهیم کرد.
ابتدا در سمت سرور یک Hub ساده را به صورت زیر تعریف می‌کنیم:
public class ChatHub : Hub
{
        public void Send(string name, string message)
        {
            Clients.All.messageReceived(name, message);
        }
}
برای سمت کلاینت نیز یک پروژه Android Application داخل Eclipse به صورت زیر ایجاد می‌کنیم:

خوب، برای استفاده از SignalR در پروژه‌ی ایجاد شده باید کتابخانه‌های زیر را به درون پوشه libs اضافه کنیم، همچنین باید ارجاعی به کتابخانه Gson نیز داشته باشیم.

قدم بعدی افزودن کدهای سمت کلاینت برای SignalR می‌باشد. دقت داشته باشید که کدهایی که در ادامه مشاهده خواهید کرد دقیقاً مطابق دستورالمعل‌هایی است که قبلاً مشاهده کرده‌اید. برای اینکار داخل کلاس MainActivity.java کدهای زیر را اضافه کنید:

Platform.loadPlatformComponent( new AndroidPlatformComponent() );
HubConnection connection = new HubConnection(DEFAULT_SERVER_URL);
HubProxy hub = connection.createHubProxy("ChatHub");
connection.error(new ErrorCallback() {
    
    @Override
    public void onError(final Throwable error) {
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(getApplicationContext(), error.getMessage(), Toast.LENGTH_LONG).show();
            }
        });
        
    }
});
hub.subscribe(new Object() {
    @SuppressWarnings("unused")
    public void messageReceived(final String name, final String message) {
        
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(getApplicationContext(), name + ": " + message, Toast.LENGTH_LONG).show();
            }
        });
    }
});
connection.start()
.done(new Action<Void>() {
    
    @Override
    public void run(Void obj) throws Exception {
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(getApplicationContext(), "Done Connecting!", Toast.LENGTH_LONG).show();
            }
        });
    }
});
connection.received(new MessageReceivedHandler() {
    @Override
    public void onMessageReceived(final JsonElement json) {
        runOnUiThread(new Runnable() {
            public void run() {
                JsonObject jsonObject = json.getAsJsonObject();
                JsonArray jsonArray =jsonObject.getAsJsonArray("A");
                Toast.makeText(getApplicationContext(), jsonArray.get(0).getAsString() + ": " + jsonArray.get(1).getAsString(), Toast.LENGTH_LONG).show();
            }
        });
    }
});

همانطور که مشاهده می‌کنید توسط قطعه کد زیر SKD مربوطه در نسخه‌های قدیمی اندروید نیز بدون مشکل کار خواهد کرد:

Platform.loadPlatformComponent( new AndroidPlatformComponent() );

در ادامه توسط متد createHubProxy ارجاعی به هابی که در سمت سرور ایجاد کردیم، داده‌ایم:

HubProxy hub = connection.createHubProxy("ChatHub");

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

connection.error(new ErrorCallback() {
    
    @Override
    public void onError(final Throwable error) {
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(getApplicationContext(), error.getMessage(), Toast.LENGTH_LONG).show();
            }
        });
        
    }
});
در ادامه نیز توسط کد زیر متد پویایی که در سمت سرور ایجاد کرده بودیم را جهت برقراری ارتباط با سرور اضافه کرده‌ایم:
hub.subscribe(new Object() {
    @SuppressWarnings("unused")
    public void messageReceived(final String name, final String message) {
        
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(getApplicationContext(), name + ": " + message, Toast.LENGTH_LONG).show();
            }
        });
    }
});
برای برقراری ارتباط نیز کدهای زیر را اضافه کرده‌ایم. یعنی به محض اینکه با موفقیت اتصال با سرور برقرار شد پیامی بر روی صفحه‌نمایش ظاهر می‌شود:
connection.start()
.done(new Action<Void>() {
    
    @Override
    public void run(Void obj) throws Exception {
        runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(getApplicationContext(), "Done Connecting!", Toast.LENGTH_LONG).show();
            }
        });
    }
});
در نهایت نیز برای نمایش اطلاعات دریافت شده کد زیر را نوشته‌ایم:
connection.received(new MessageReceivedHandler() {
    @Override
    public void onMessageReceived(final JsonElement json) {
        runOnUiThread(new Runnable() {
            public void run() {
                JsonObject jsonObject = json.getAsJsonObject();
                JsonArray jsonArray =jsonObject.getAsJsonArray("A");
                Toast.makeText(getApplicationContext(), jsonArray.get(0).getAsString() + ": " + jsonArray.get(1).getAsString(), Toast.LENGTH_LONG).show();
            }
        });
    }
});
همانطور که عنوان شد کدهای فوق دقیقاً براساس قواعد و دستورالعمل استفاده از SignalR در سمت کلاینت می‌باشد.

بازخوردهای دوره
تزریق وابستگی‌های AutoMapper در لایه سرویس برنامه
من تنظیمات تزریق وابستگی مربوط به AutoMapper را در همان محل قرارگیری تزریق وابستگی Serviceها قرار داده ام و ObjectFactory به این شکل شد:
public static class WoObjectFactory
    {
        private static readonly Lazy<Container> ContainerBuilder =
           new Lazy<Container>(DefaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);

        public static IContainer Container
        {
            get { return ContainerBuilder.Value; }
        }

        private static Container DefaultContainer()
        {
            
            var _container = new Container(x =>
            {
                var platformSpecificRegistry = PlatformAdapter.Resolve<IPlatformSpecificMapperRegistry>();
                platformSpecificRegistry.Initialize();

                x.For<ConfigurationStore>().Singleton().Use<ConfigurationStore>().Ctor<IEnumerable<IObjectMapper>>().Is(MapperRegistry.Mappers);
                x.For<IConfigurationProvider>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
                x.For<IConfiguration>().Use(ctx => ctx.GetInstance<ConfigurationStore>());
                x.For<ITypeMapFactory>().Use<TypeMapFactory>();
                x.For<IMappingEngine>().Singleton().Use<MappingEngine>().SelectConstructor(() => new MappingEngine(null));

                x.For<IUnitOfWork>().HybridHttpOrThreadLocalScoped().Use(() => new WirelessOrganizationContext());

                x.Scan(scan =>
                {
                    scan.AssemblyContainingType<IDeviceService>();
                    scan.TheCallingAssembly();
                    scan.WithDefaultConventions();
                });
                x.Scan(scanner =>
                {
                    scanner.AssembliesFromApplicationBaseDirectory();

                    scanner.ConnectImplementationsToTypesClosing(typeof(ITypeConverter<,>))
                           .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());

                    scanner.ConnectImplementationsToTypesClosing(typeof(ValueResolver<,>))
                        .OnAddedPluginTypes(t => t.HybridHttpOrThreadLocalScoped());
                });

                x.Scan(scan =>
                {
                    scan.TheCallingAssembly();
                    scan.WithDefaultConventions();
                    scan.AddAllTypesOf<Profile>().NameBy(item => item.FullName);
                });

                

            });
            ConfigureAutoMapper(_container);
            return _container;
        }

        private static void ConfigureAutoMapper(IContainer container)
        {
            var configuration = container.TryGetInstance<IConfiguration>();
            if (configuration == null) return;
            //saying AutoMapper how to resolve services
            configuration.ConstructServicesUsing(container.GetInstance);
            foreach (var profile in container.GetAllInstances<Profile>())
            {
                configuration.AddProfile(profile);
            }
        }
    }
اما وقتی پروژه اجرا می‌شود اکسپشن زیر اتفاق می‌افتد، که البته بعد از اضافه کردن تنظیمات مربوط به تزریق وابستگی AutoMapper اتفاق افتاد، در این Value مربوط به ContainerBuilder  مقداری ندارد: