مطالب
یافتن اکشن متد‌های Post ایی در ASP.NET MVC که فیلتر CSRF ندارند
روش مرسوم مقابله با حملات CSRF در ASP.NET MVC، استفاده از فیلتر امنیتی ValidateAntiForgeryToken بر روی اکشن متدهایی از نوع Post است و سپس فراخوانی Html.AntiForgeryToken در View متناظر.
با بالا رفتن تعداد اکشن متدهای یک پروژه، ممکن است استفاده از ValidateAntiForgeryToken فراموش شود. در ادامه مثالی را ملاحظه می‌کنید که یک پروژه‌ی ASP.NET MVC را جهت یافتن اکشن متدهای Post ایی که فیلتر ValidateAntiForgeryToken ندارند، اسکن می‌کند:

using System;
using System.Linq;
using System.Reflection;
// Add a ref. to \Program Files\Microsoft ASP.NET\ASP.NET MVC 4\Assemblies\System.Web.Mvc.dll
using System.Web.Mvc;
// Add a ref. to System.Web
using System.Web.UI;

namespace FindOutputCaches
{
    class Program
    {
        static void Main(string[] args)
        {
            var path = @"D:\path\bin\site.dll";
            var asmTarget = Assembly.LoadFrom(path);

            checkCsrfTokens(asmTarget);

            Console.WriteLine("Press a key...");
            Console.Read();
        }


        private static void checkCsrfTokens(Assembly asmTarget)
        {
            // یافتن کلیه کنترلرها
            var controllers = asmTarget.GetTypes()
                                       .Where(type => typeof(IController).IsAssignableFrom(type) &&
                                                     !type.Name.StartsWith("T4MVC"))
                                       .ToList();

            foreach (var controller in controllers)
            {
                // یافتن کلیه اکشن متدهای کنترلر جاری
                var actionMethods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
                                              .Where(method => typeof(ActionResult).IsAssignableFrom(method.ReturnType))
                                              .ToList();

                foreach (var method in actionMethods)
                {                    
                    var httpPostAttributes = method.GetCustomAttributes(typeof(HttpPostAttribute), true);
                    if (httpPostAttributes == null || !httpPostAttributes.Any())
                        continue;

                    var csrfTokens = method.GetCustomAttributes(typeof(ValidateAntiForgeryTokenAttribute), true);
                    if (csrfTokens == null || !csrfTokens.Any())
                    {
                        Console.WriteLine("Detected [HttpPost] without [ValidateAntiForgeryToken] in:\n {0}-->{1}",
                                               controller.FullName, method.Name);
                    }
                }
            }
        }
    }
}
ابتدا مسیر اسمبلی کامپایل شده پروژه ASP.NET MVC که حاوی کنترلرهای برنامه است، باید مشخص گردد.
سپس در این اسمبلی، کلیه نوع‌های تعریف شده، یافت گردیده و آن‌هایی که پیاده سازی کننده IController هستند (یعنی کلاس‌های کنترلر واقعی برنامه)، جدا خواهند شد.
در ادامه در این کنترلرها، متدهایی را بررسی خواهیم کرد که دارای خروجی از نوع ActionResult باشند (فقط اکشن متدها مدنظر هستند). اگر این اکشن متد یافت شده دارای ویژگی HttpPost بود و همچنین ValidateAntiForgeryToken نداشت، یعنی یک مشکل امنیتی که باید برطرف شود.
مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت سوم - اعتبارسنجی سمت کلاینت
FluentValidation یک کتابخانه‌ی اعتبارسنجی اطلاعات سمت سرور است و راهکاری را برای اعتبارسنجی‌های سمت کلاینت ارائه نمی‌دهد؛ اما می‌تواند متادیتای مورد نیاز unobtrusive java script validation را تولید کند و به این ترتیب بسیاری از قواعد آن دقیقا همانند روش‌های توکار اعتبارسنجی ASP.NET Core در سمت کلاینت نیز کار می‌کنند؛ مانند:
• NotNull/NotEmpty
• Matches (regex)
• InclusiveBetween (range)
• CreditCard
• Email
• EqualTo (cross-property equality comparison)
• MaxLength
• MinLength
• Length

اما باید دقت داشت که سایر قابلیت‌های این کتابخانه، قابلیت ترجمه‌ی به قواعد unobtrusive java script validation ندارند؛ مانند:
- استفاده‌ی از شرط‌ها توسط متدهای When و یا Unless (برای مثال اگر خاصیت A دارای مقدار 5 بود، آنگاه ورود اطلاعات خاصیت B باید اجباری شود)
- تعریف قواعد اعتبارسنجی سفارشی، برای مثال توسط متد Must
- تعریف RuleSetها (برای مثال تعیین کنید که از یک مدل، فقط تعدادی از خواص آن اعتبارسنجی شوند و نه تمام آن‌ها)

در یک چنین حالت‌هایی باید کاربر اطلاعات کامل فرم را به سمت سرور ارسال کند و در post-back صورت گرفته، نتایج اعتبارسنجی را دریافت کند.


روش توسعه‌ی اعتبارسنجی سمت کلاینت کتابخانه‌ی FluentValidation جهت سازگاری آن با  unobtrusive java script validation

باتوجه به اینکه قواعد سفارشی کتابخانه‌ی FluentValidation، معادل unobtrusive java script validation ای ندارند، در ادامه مثالی را جهت تدارک آن‌ها بررسی می‌کنیم. برای این منظور، معادل مطلب «ایجاد ویژگی‌های اعتبارسنجی سفارشی در ASP.NET Core 3.1 به همراه اعتبارسنجی سمت کلاینت آن‌ها» را بر اساس ویژگی‌های کتابخانه‌ی FluentValidation پیاده سازی می‌کنیم.

1) ایجاد اعتبارسنج سمت سرور

می‌خواهیم اگر مقدار عددی خاصیتی بیشتر از مقدار عددی خاصیت دیگری بود، اعتبارسنجی آن با شکست مواجه شود. به همین منظور با پیاده سازی یک PropertyValidator سفارشی، LowerThanValidator را به صورت زیر تعریف می‌کنیم:
using System;
using FluentValidation;
using FluentValidation.Validators;

namespace FluentValidationSample.Models
{
    public class LowerThanValidator : PropertyValidator
    {
        public string DependentProperty { get; set; }

        public LowerThanValidator(string dependentProperty) : base($"باید کمتر از {dependentProperty} باشد")
        {
            DependentProperty = dependentProperty;
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            if (context.PropertyValue == null)
            {
                return false;
            }

            var typeInfo = context.Instance.GetType();
            var dependentPropertyValue =
                Convert.ToInt32(typeInfo.GetProperty(DependentProperty).GetValue(context.Instance, null));

            return int.Parse(context.PropertyValue.ToString()) < dependentPropertyValue;
        }
    }

    public static class CustomFluentValidationExtensions
    {
        public static IRuleBuilderOptions<T, int> LowerThan<T>(
            this IRuleBuilder<T, int> ruleBuilder, string dependentProperty)
        {
            return ruleBuilder.SetValidator(new LowerThanValidator(dependentProperty));
        }
    }
}
- در اینجا dependentProperty همان خاصیت دیگری است که باید مقایسه با آن صورت گیرد. در متد بازنویسی شده‌ی IsValid، می‌توان مقدار آن‌را از context.Instance استخراج کرده و سپس با مقدار جاری context.PropertyValue مقایسه کرد.
- همچنین جهت سهولت کار فراخوانی آن، یک متد الحاقی جدید به نام LowerThan نیز در اینجا تعریف شده‌است.
- البته اگر قصد اعتبارسنجی سمت سرور را ندارید، می‌توان در متد IsValid، مقدار true را بازگشت داد.


2) ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور

در ادامه نیاز است ویژگی‌های data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Internal;
using FluentValidation.Resources;
using FluentValidation.Validators;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace FluentValidationSample.Models
{
    public class LowerThanClientValidator : ClientValidatorBase
    {
        private LowerThanValidator LowerThanValidator
        {
            get { return (LowerThanValidator)Validator; }
        }

        public LowerThanClientValidator(PropertyRule rule, IPropertyValidator validator) :
            base(rule, validator)
        {
        }

        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-LowerThan", GetErrorMessage(context));
            MergeAttribute(context.Attributes, "data-val-LowerThan-dependentproperty", LowerThanValidator.DependentProperty);
        }

        private string GetErrorMessage(ClientModelValidationContext context)
        {
            var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName());
            string messageTemplate;
            try
            {
                messageTemplate = Validator.Options.ErrorMessageSource.GetString(null);
            }
            catch (FluentValidationMessageFormatException)
            {
                messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>();
            }
            return formatter.BuildMessage(messageTemplate);
        }
    }
}
در این کدها، تنها قسمت مهم آن، متد AddValidation است که کار تعریف و افزودن متادیتاهای unobtrusive java script validation را انجام می‌دهد و برای مثال سبب رندر یک چنین تگ HTML ای می‌شود که در آن پیام اعتبارسنجی و خاصیت دیگر مدنظر ذکر شده‌اند:
<input dir="rtl" type="number"
 data-val="true"
 data-val-lowerthan="باید کمتر از Age باشد"
 data-val-lowerthan-dependentproperty="Age"
 data-val-required="'سابقه کار' must not be empty."
 id="Experience"
 name="Experience"
 value="">


3) تعریف مدل کاربران و اعتبارسنجی آن

مدلی که در این مثال از آن استفاده شده، یک چنین تعریفی را دارد و در قسمت اعتبارسنجی خاصیت Experience آن، از متد الحاقی جدید LowerThan استفاده شده‌است:
    public class UserModel
    {
        [Display(Name = "نام کاربری")]
        public string Username { get; set; }

        [Display(Name = "سن")]
        public int Age { get; set; }

        [Display(Name = "سابقه کار")]
        public int Experience { get; set; }
    }

    public class UserValidator : AbstractValidator<UserModel>
    {
        public UserValidator()
        {
            RuleFor(x => x.Username).NotNull();
            RuleFor(x => x.Age).NotNull();
            RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull();
        }
    }


4) افزودن اعتبارسنج‌های تعریف شده به تنظیمات برنامه

پس از تعریف LowerThanValidator و LowerThanClientValidator، روش افزودن آن‌ها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews().AddFluentValidation(
                fv =>
                {
                    fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
                    fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
                    fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;

                    fv.ConfigureClientsideValidation(clientSideValidation =>
                    {
                        clientSideValidation.Add(
                            validatorType: typeof(LowerThanValidator),
                            factory: (context, rule, validator) => new LowerThanClientValidator(rule, validator));
                    });
                }
            );
        }


5) تعریف کدهای جاوا اسکریپتی مورد نیاز

پیش از هرکاری، اسکریپت‌های فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
در اینجا مداخل jquery، سپس jquery.validate و بعد از آن jquery.validate.unobtrusive را مشاهده می‌کنید. در ادامه فایل js/site.js را به صورت زیر تکمیل خواهیم کرد:
$.validator.unobtrusive.adapters.add('LowerThan', ['dependentproperty'], function (options) {
    options.rules['LowerThan'] = {
        dependentproperty: options.params['dependentproperty']
    };
    options.messages['LowerThan'] = options.message;
});

$.validator.addMethod('LowerThan', function (value, element, parameters) {      
    var dependentProperty = '#' + parameters['dependentproperty'];
    var dependentControl = $(dependentProperty);
    if (dependentControl) {
        var targetvalue = dependentControl.val();
        if (parseInt(targetvalue) > parseInt(value)) {
            return true;
        }
        return false;
    }
    return true;
});
در اینجا یکبار اعتبارسنج LowerThan را به validator.unobtrusive معرفی می‌کنیم. سپس منطق پیاده سازی آن‌را که بر اساس یافتن مقدار خاصیت دیگر و مقایسه‌ی آن با مقدار خاصیت جاری است، به اعتبارسنج‌های jQuery Validator اضافه خواهیم کرد.


6) آزمایش برنامه

پس از این تنظیمات، اگر Viewما چنین تعریفی را داشته باشد:
@using FluentValidationSample.Models
@model UserModel
@{
    ViewData["Title"] = "Home Page";
}
<div dir="rtl">
    <form asp-controller="Home"
          asp-action="RegisterUser"
          method="post">
        <fieldset class="form-group">
        <legend>ثبت نام</legend>
            <div class="form-group row">
                <label asp-for="Username" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Username" class="form-control" />
                    <span asp-validation-for="Username" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group row">
                <label asp-for="Age" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Age" class="form-control" />
                    <span asp-validation-for="Age" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group row">
                <label asp-for="Experience" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Experience" class="form-control" />
                    <span asp-validation-for="Experience" class="text-danger"></span>
                </div>
            </div>

            <div class="form-group row">
                <label class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10 text-md-right">
                    <button type="submit" class="btn btn-info col-md-2">ارسال</button>
                </div>
            </div>
        </fieldset>
    </form>
</div>
در صورتیکه سابقه‌ی کار را بیشتر از سن وارد کنیم، به یک چنین خروجی سمت کلاینتی (بدون نیاز به post-back کامل به سمت سرور) خواهیم رسید:

مطالب
نحوه نمایش فیلدهای تصویری و همچنین بارگذاری تصاویر از مسیری مشخص در PdfReport
عموما برای نمایش تصاویر در گزارشات، یکی از دو حالت زیر وجود دارد:
الف) مسیر تصاویر موجود در فایل سیستم، در بانک اطلاعاتی ذخیره شده‌اند و قرار است گزارش نهایی در ستونی مشخص شامل این تصاویر باشد.
ب) محتوای بایناری تصاویر در خود بانک اطلاعاتی ذخیره شده‌اند و نیاز است آن‌ها را در گزارشات نمایش دهیم.

هر دو حالت فوق در PdfReport پشتیبانی می‌شوند و در ادامه نحوه انجام این موارد را بررسی خواهیم کرد.

الف) بارگذاری تصاویر از فایل سیستم

ابتدا مدل زیر را در نظر بگیرید:
namespace PdfReportSamples.Models
{
    public class ImageRecord
    {
        public int Id { set; get; }
        public string ImagePath { set; get; }
        public string Name { set; get; }
    }
}
توسط آن تعدادی رکورد را که ImagePath آن‌ها فایل‌هایی بر روی سیستم هستند، خواهیم ساخت:
using System;
using System.Collections.Generic;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.ImageFilePath
{
    public class ImageFilePathPdfReport
    {
        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(AppPath.ApplicationPath + "\\fonts\\irsans.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.SnowyPineTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<ImageRecord>
                                             {
                                                 new ImageRecord
                                                     {
                                                         Id=1,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\01.png",
                                                         Name = "Rnd"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=2,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\02.png",
                                                         Name = "Bug"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=3,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\03.png",
                                                         Name = "Stuff"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=4,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\04.png",
                                                         Name = "Sun"
                                                     }
                                             };
                dataSource.StronglyTypedList(listOfRows);
            })
            .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<ImageRecord>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(3);
                    column.HeaderCell("شماره");
                    column.ColumnItemsTemplate(t => t.Barcode(new Barcode39()));
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.ImagePath);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("تصویر");
                    column.ColumnItemsTemplate(t => t.ImageFilePath(defaultImageFilePath: string.Empty, fitImages: false));
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("نام");
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptImageFilePathSample.pdf"));
        }
    }
}

توضیحات:
- در متد MainTableDataSource، یک سری رکورد دلخواه را بر اساس تعدادی تصویر مشخص ایجاد کرده‌ایم. این تصاویر در پوشه bin\images مثال‌های PdfReport قرار دارند. سپس آن‌ها را توسط متد dataSource.StronglyTypedList، در اختیار PdfReport قرار داده‌ایم.
- در مرحله بعد، توسط متد MainTableColumns، ستون‌های دلخواه گزارش را ایجاد کرده‌ایم.
دو نکته در اینجا مهم هستند:
الف) برای نمایش یک تصویر از فایل سیستم، فقط کافی است از ColumnItemsTemplate متناظر با آن استفاده کرد (حالت پیش فرض، نمایش متنی اطلاعات است). برای نمونه قالب پیش فرض ImageFilePath به نحو زیر قابل استفاده است:
 column.ColumnItemsTemplate(t => t.ImageFilePath(defaultImageFilePath: string.Empty, fitImages: false));
در اینجا در آرگومان اول آن می‌توان مسیر تصویری را مشخص کرد که در صورت موجود نبودن تصویر مشخص شده در منبع داده، بهتر است نمایش داده شود. اگر string.Empty وارد شد، از این مورد صرفنظر خواهد شد. آرگومان دوم آن مشخص می‌کند که آیا تصویر نمایش داده شده باید با ابعاد سلول متناظر با آن هماهنگ شده و نمایش داده شود یا خیر.
ب) در کتابخانه iTextSharp که پایه PdfReport است، امکان تهیه بارکد هم وجود دارد. برای مثال این بارکدها توسط قالب زیر، قابل استفاده خواهند شد:
 column.ColumnItemsTemplate(t => t.Barcode(new Barcode39()));
تصویری از حاصل این گزارش را در ذیل مشاهده می‌کنید:


ب) بارگذاری محتوای تصاویر از بانک اطلاعاتی

ابتدا کدهای کامل این مثال را در نظر بگیرید:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.DbImage
{
    public class DbImagePdfReport
    {
        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(AppPath.ApplicationPath + "\\fonts\\irsans.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.AutumnTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.FitToContent);
             })
             .MainTableDataSource(dataSource =>
             {
                 dataSource.GenericDataReader(
                   providerName: "System.Data.SQLite",
                   connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                   sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate], [thumbnail]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                   parametersValues: new object[] { 10 }
               );
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع کل");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.HeaderCell("ردیف");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("url");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.HeaderCell("آدرس");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("name");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(2);
                     column.HeaderCell("نام");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("NumberOfPosts");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     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));
                     });
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("AddDate");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                     column.HeaderCell("تاریخ ثبت");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : ((DateTime)obj).ToString("dd/MM/yyyy HH:mm"));
                     });
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("thumbnail");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(5);
                     column.HeaderCell("تصویر");
                     column.ColumnItemsTemplate(t => t.ByteArrayImage(defaultImageFilePath: string.Empty, fitImages: false));
                 });
             })
             .Export(export =>
             {
                 export.ToXml();
                 export.ToExcel();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptDbImageSample.pdf"));
        }
    }
}

توضیحات:
بانک اطلاعاتی SQLite موجود در پوشه bin\data سورس PdfReport، حاوی فیلدی است به نام thumbnail که در آن محتوای یک سری تصویر ذخیره شده‌اند.
در اینجا توسط کوئری زیر قصد داریم این اطلاعات را خوانده و نمایش دهیم:
SELECT [url], [name], [NumberOfPosts], [AddDate], [thumbnail]
FROM [tblBlogs]
WHERE [NumberOfPosts]>=@p1
همانطور که ملاحظه می‌کنید، در اینجا نیز تنها کافی است قالب ستون مرتبطی را انتخاب کنیم:
column.ColumnItemsTemplate(t => t.ByteArrayImage(defaultImageFilePath: string.Empty, fitImages: false));
قالب ByteArrayImage، اطلاعات باینری فیلد thumbnail را خوانده و تبدیل به تصاویر قرار داده شده در فایل گزارش نهایی می‌کند. پارامترهای آن، با پارامترهای قالب ImageFilePath که پیشتر توضیح داده شد، یکی هستند. 
نظرات مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
باید از Dictionary استفاده کنید برای تعریف خواص پویا:
public class Field
{
  [JsonExtensionData]
  public Dictionary<string, object> Property { get; set; }
}
 
public class FieldType
{
  public string Type { get; set; }
}
و بعد نحوه استفاده از آن به صورت زیر خواهد بود:
var data = new
{
  model = new
  {
   fields = new List<Field>
   {
    new Field
    {
      Property = new Dictionary<string, object>
      {
       {"Id", new FieldType { Type = "number" }},
       {"Name", new FieldType { Type = "string" }}
      }
    }
   }
  }
};
var dataJson = JsonConvert.SerializeObject(data, Formatting.Indented);
با این خروجی:
{
  "model": {
    "fields": [
      {
        "Id": {
          "Type": "number"
        },
        "Name": {
          "Type": "string"
        }
      }
    ]
  }
}
- اگر از Web API استفاده می‌کنید، ذکر سطر JsonConvert.SerializeObject ضروری نیست و به صورت توکار از JSON.NET‌ استفاده می‌کند.
- اگر از ASP.NET MVC استفاده می‌کنید، نیاز است از آن کمک بگیرید. از این جهت که خاصیت JsonExtensionData سبب می‌شود تا نام ثابت خاصیت Property، از خروجی نهایی حذف شود و اعضای دیکشنری، جزئی از خاصیت‌های موجود شوند.
- نکته‌ی «گرفتن خروجی CamelCase از JSON.NET» را هم باید مد نظر داشته باشید.
مطالب دوره‌ها
لغو Lazy Loading در حین کار با AutoMapper و Entity Framework
پیشنیازها
- مطالعه‌ی مطالب گروه AutoMapper در سایت، دید خوبی را برای شروع به کار با آن فراهم می‌کنند و در اینجا قصد تکرار این مباحث پایه‌ای را نخواهیم داشت. هدف بیشتر بررسی یک سری نکات پیشرفته‌تر و عمیق‌تر است از کار با AutoMapper.
- آشنایی با Lazy loading و Eager loading در حین کار با EF


ساختار و پیشنیازهای برنامه‌ی مطلب جاری

جهت سهولت پیگیری مطلب و تمرکز بیشتر بر روی مفاهیم اصلی مورد بحث، یک برنامه‌ی کنسول را آغاز کرده و سپس بسته‌های نیوگت ذیل را به آن اضافه کنید:
PM> install-package AutoMapper
PM> install-package EntityFramework
به این ترتیب بسته‌های AutoMapper و EF به پروژه‌ی جاری اضافه خواهند شد.


آشنایی با ساختار مدل‌های برنامه

در اینجا ساختار جداول مطالب یک بلاگ را به همراه نویسندگان آن‌ها، مشاهده می‌کنید:
public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
 
    [ForeignKey("UserId")]
    public virtual User User { get; set; }
    public int UserId { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
 
    public virtual ICollection<BlogPost> BlogPosts { get; set; }
}
هر کاربر می‌تواند تعدادی مطلب تهیه کند و هر مطلب توسط یک کاربر نوشته شده‌است.


هدف از این مثال

فرض کنید اطلاعاتی که قرار است به کاربر نمایش داده شوند، توسط ViewModel ذیل تهیه می‌شود:
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
 
    public ICollection<BlogPost> BlogPosts { get; set; }
}
در اینجا می‌خواهیم اولین کاربر ثبت شده را یافته و سپس لیست مطالب آن‌را نمایش دهیم. همچنین می‌خواهیم این کوئری تهیه شده به صورت خودکار اطلاعاتش را بر اساس ساختار ViewModel ایی که مشخص کردیم (و این ViewModel الزاما تمام عناصر آن با عناصر مدل اصلی یکی نیست)، بازگشت دهیم.


تهیه نگاشت‌های AutoMapper

برای مدیریت بهتر نگاشت‌های AutoMapper توصیه شده‌است که کلاس‌های Profile ایی را به شکل ذیل تهیه کنیم:
public class TestProfile : Profile
{
    protected override void Configure()
    {
        this.CreateMap<User, UserViewModel>();
    }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
کار با ارث بری از کلاس پایه Profile کتابخانه‌ی AutoMapper شروع می‌شود. سپس باید متد Configure آن‌را بازنویسی کنیم. در اینجا می‌توان با استفاده از متدی مانند Create مشخص کنیم که قرار است اطلاعاتی با ساختار شیء User، به اطلاعاتی با ساختار از نوع شیء UserViewModel به صورت خودکار نگاشت شوند.


ثبت و معرفی پروفایل‌های AutoMapper

پس از تهیه‌ی پروفایل مورد نیاز، در ابتدای برنامه با استفاده از متد Mapper.Initialize، کار ثبت این تنظیمات صورت خواهد گرفت:
Mapper.Initialize(cfg => // In Application_Start()
{
    cfg.AddProfile<TestProfile>();
});


روش متداول کار با AutoMapper جهت نگاشت اطلاعات User به ViewModel آن

در ادامه به نحو متداولی، ابتدا اولین کاربر ثبت شده را یافته و سپس با استفاده از متد Mapper.Map اطلاعات این شیء user به ViewModel آن نگاشت می‌شود:
using (var context = new MyContext())
{
    var user1 = context.Users.FirstOrDefault();
    if (user1 != null)
    {
        var uiUser = new UserViewModel();
        Mapper.Map(source: user1, destination: uiUser);
 
        Console.WriteLine(uiUser.Name);
        foreach (var post in uiUser.BlogPosts)
        {
            Console.WriteLine(post.Title);
        }
    }
}
تا اینجا اگر برنامه را اجرا کنید، مشکلی را مشاهده نخواهید کرد، اما این کدها سبب اجرای حداقل دو کوئری خواهند شد:
الف) یافتن اولین کاربر
ب) واکشی لیست مطالب او در یک کوئری دیگر


کاهش تعداد رفت و برگشت‌ها به سرور با استفاده از متدهای ویژه‌ی AutoMapper

در حالت متداول کار با EF، با استفاده از متد Include می‌توان این Lazy loading را لغو کرد و در همان اولین کوئری، مطالب کاربر یافت شده را نیز دریافت نمود:
 var user1 = context.Users.Include(user => user.BlogPosts).FirstOrDefault();
و سپس این اطلاعات را توسط AutoMapper نگاشت کرد.
در این حالت، AutoMapper برای ساده سازی این مراحل، متدهای Project To را معرفی کرده‌است:
 var uiUser = context.Users.Project().To<UserViewModel>().FirstOrDefault();
در اینجا نیز Lazy loading لغو شده و به صورت خودکار جوینی به جدول مطالب کاربران ایجاد خواهد شد.
بنابراین با استفاده از متد‌های Project To می‌توان از ذکر Includeهای EF صرفنظر کرد و همچنین دیگر نیازی به نوشتن متد Select جهت نگاشت دستی خواص مورد نظر به خواص ViewModel نیست.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample01.zip
مطالب
ASP.NET MVC #15

فیلترها در ASP.NET MVC

پایه قسمت‌های بعدی مانند مباحث امنیت، اعتبار سنجی کاربران، caching و غیره، مبحثی است به نام فیلترها در ASP.NET MVC. تابحال با سه فیلتر به نام‌های ActionName، NonAction و AcceptVerbs آشنا شده‌ایم. به این‌ها Action selector filters هم گفته می‌شود. زمانیکه قرار است یک درخواست رسیده به متدی در یک کنترلر خاص نگاشت شود،‌ فریم ورک ابتدا به متادیتای اعمالی به متدها توجه کرده و بر این اساس درخواست را به متدی صحیح هدایت خواهد کرد. ActionName، نام پیش فرض یک متد را بازنویسی می‌کند و توسط AcceptVerbs اجرای یک متد، به افعالی مانند POST، GET، DELETE و امثال آن محدود می‌شود که در قسمت‌های قبل در مورد آن‌ها بحث شد.
علاوه بر این‌ها یک سری فیلتر دیگر نیز در ASP.NET MVC وجود دارند که آن‌ها نیز به شکل متادیتا به متدهای کنترلرها اعمال شده و کار نهایی‌اشان تزریق کدهایی است که باید پیش و پس از اجرای یک اکشن متد،‌ اجرا شوند. 4 نوع فیلتر در ASP.NET MVC وجود دارند:
الف) IAuthorizationFilter
این نوع فیلترها پیش از اجرای هر متد یا فیلتر دیگری در کنترلر جاری اجرا شده و امکان لغو اجرای آن‌را فراهم می‌کنند. پیاده سازی پیش‌فرض آن توسط کلاس AuthorizeAttribute در فریم ورک وجود دارد.
بدیهی است این نوع اعمال را مستقیما داخل متدهای کنترلرها نیز می‌توان انجام داد (بدون نیاز به هیچگونه فیلتری). اما به این ترتیب حجم کدهای تکراری در سراسر برنامه به شدت افزایش می‌یابد و نگهداری آن‌را در طول زمان مشکل خواهد ساخت.

ب) IActionFilter
ActionFilterها پیش (OnActionExecuting) و پس از (OnActionExecuted) اجرای متدهای کنترلر جاری اجرا می‌شوند و همچنین پیش از ارائه خروجی نهایی متدها. به این ترتیب برای مثال می‌توان نحوه رندر یک View را تحت کنترل گرفت. این اینترفیس توسط کلاس ActionFilterAttribute در فریم ورک پیاده سازی شده است.

ج) IResultFilter
ResultFilter بسیار شبیه به ActionFilter است با این تفاوت که تنها پیش از (OnResultExecuting) بازگرداندن نتیجه متد و همچنین پس از (OnResultExecuted) اجرای متد، فراخوانی می‌گردد. کلاس ActionFilterAttribute موجود در فریم ورک، پیاده سازی پیش فرضی از آن‌‌را ارائه می‌دهد.

د) IExceptionFilter
ExceptionFilterها پس از اجرای تمامی فیلترهای دیگر، همواره اجرا خواهند شد؛ صرفنظر از اینکه آیا در این بین استثنایی رخ داده است یا خیر. بنابراین یکی از کاربردهای آن‌ها می‌تواند ثبت وقایع مرتبط با استثناهای رخ‌داده باشد. پیاده سازی پیش فرض آن توسط کلاس HandleErrorAttribute در فریم ورک موجود است.

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

مثالی جهت درک بهتر ترتیب و نحوه اجرای فیلترها:

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس فیلتر سفارشی زیر را به برنامه اضافه نمائید:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class LogAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Log("OnActionExecuting", filterContext);
}

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
Log("OnActionExecuted", filterContext);
}

public override void OnResultExecuting(ResultExecutingContext filterContext)
{
Log("OnResultExecuting", filterContext);
}

public override void OnResultExecuted(ResultExecutedContext filterContext)
{
Log("OnResultExecuted", filterContext);
}

private void Log(string stage, ControllerContext ctx)
{
ctx.HttpContext.Response.Write(
string.Format("{0}:{1} - {2} < br/> ",
ctx.RouteData.Values["controller"], ctx.RouteData.Values["action"], stage));
}
}
}

مرسوم است برای ایجاد فیلترهای سفارشی، همانند مثال فوق با ارث بری از پیاده سازی‌های توکار اینترفیس‌های چهارگانه یاد شده، کار شروع شود.
سپس یک کنترلر جدید را به همراه دو متد، به برنامه اضافه نمائید. برای هر کدام از متدها هم یک View خالی را ایجاد کنید. اکنون این ویژگی جدید را به هر کدام از این متدها اعمال نموده و برنامه را اجرا کنید.

using System.Web.Mvc;
using MvcApplication12.CustomFilters;

namespace MvcApplication12.Controllers
{
public class HomeController : Controller
{
[Log]
public ActionResult Index()
{
return View();
}

[Log]
public ActionResult Test()
{
return View();
}
}
}

سپس ویژگی Log را از متدها حذف کرده و به خود کنترلر اعمال کنید:
[Log]
public class HomeController : Controller

در این حالت ویژگی اعمالی، پیش از اجرای متد درخواستی جاری اجرا خواهد شد یا به عبارتی به تمام متدهای قابل دسترسی کنترلر اعمال می‌گردد.


تقدم و تاخر اجرای فیلترهای هم‌خانواده

همانطور که عنوان شد، همیشه ابتدا AuthorizationFilter اجرا می‌شود و در آخر ExceptionFilter. سؤال: اگر در این بین مثلا دو نوع ActionFilter متفاوت به یک متد اعمال شدند، کدامیک ابتدا اجرا می‌شود؟
تمام فیلترها از کلاسی به نام FilterAttribute مشتق می‌شوند که دارای خاصیتی است به نام Order. بنابراین جهت مشخص سازی ترتیب اجرای فیلترها تنها کافی است این خاصیت مقدار دهی شود. برای مثال جهت اعمال دو فیلتر سفارشی زیر:

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class AuthorizationFilterA : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
Debug.WriteLine("OnAuthorization : AuthorizationFilterA");
}
}
}

using System.Diagnostics;
using System.Web.Mvc;

namespace MvcApplication12.CustomFilters
{
public class AuthorizationFilterB : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
Debug.WriteLine("OnAuthorization : AuthorizationFilterB");
}
}
}

خواهیم داشت:
using System.Web.Mvc;
using MvcApplication12.CustomFilters;

namespace MvcApplication12.Controllers
{
public class HomeController : Controller
{
[AuthorizationFilterA(Order = 2)]
[AuthorizationFilterB(Order = 1)]
public ActionResult Index()
{
return View();
}
}
}

در اینجا با توجه به مقادیر order، ابتدا AuthorizationFilterB اجرا می‌گردد و سپس AuthorizationFilterA.
علاوه بر این‌ها محدوده اجرای فیلترها نیز بر بر این حق تقدم اجرایی تاثیر گذار هستند. برای مثال در پشت صحنه زمانیکه قرار است یک فیلتر جدید اجرا شود، وهله سازی آن به نحوه زیر است که بر اساس مقادیر order و FilterScope صورت می‌گیرد:
var filter = new Filter(actionFilter, FilterScope, order);

مقادیر FilterScope را در ادامه ملاحظه می‌نمائید:
namespace System.Web.Mvc { 
public enum FilterScope {
First = 0,
Global = 10,
Controller = 20,
Action = 30,
Last = 100,
}
}

به صورت پیش فرض، ابتدا فیلتری با محدوده اجرای کمتر، اجرا خواهد شد. در اینجا Global به معنای اجرای شدن در تمام کنترلرها است.


تعریف فیلترهای سراسری

برای اینکه فیلتری را عمومی و سراسری تعریف کنیم، تنها کافی است آن‌را در متد Application_Start فایل Global.asax.cs به نحو زیر معرفی نمائیم:

GobalFilters.Filters.Add(new AuthorizationFilterA() { Order = 2});

به این ترتیب AuthorizationFilterA، به تمام کنترلرها و متدهای قابل دسترسی آن‌ها در برنامه به صورت خودکار اعمال خواهد شد.
یکی از کاربردهای فیلترهای سراسری، نوشتن برنامه‌های پروفایلر است. برنامه‌هایی که برای مثال مدت زمان اجرای متدها را ثبت کرده و بر این اساس بهتر می‌توان کارآیی قسمت‌های مختلف برنامه را دقیقا زیرنظر قرار داد.


یک نکته
کلاس کنترلر در ASP.NET MVC نیز یک فیلتر است:
public abstract class Controller : ControllerBase, IActionFilter, IAuthorizationFilter, IDisposable, IExceptionFilter, IResultFilter

به همین دلیل، امکان تحریف متدهای OnActionExecuting، OnActionExecuted و امثال آن که پیشتر ذکر شد، در یک کنترلر نیز وجود دارد.
کلاس کنترلر دارای محدوده اجرایی First و Order ایی مساوی Int32.MinValue است. به این ترتیب کنترلرها پیش از اجرای هر فیلتر دیگری اجرا خواهند شد.


ASP.NET MVC دارای یک سری فیلتر و متادیتای توکار مانند OutputCache، HandleError، RequireHttps، ValidateInpute و غیره است که توضیحات بیشتر آن‌ها به قسمت‌های بعد موکول می‌گردد.

مطالب
روش ذخیره‌ی لاگ‌های ILogger در پایگاه داده در Blazor

مقدمه

همانطور که می‌دانید، Blazor دارای یک سیستم لاگ گیری توکار است که می‌توان از آن توسط تزریق ILogger در کامپوننت‌ها بهره برد. این سیستم لاگ گیری در زمان توسعه‌ی نرم افزار، در قالب یک کنسول، لاگ‌ها را به توسعه دهنده نشان می‌دهد. اما پس از تولید و پابلیش اپلیکیشن، دیگر این کنسول وجود ندارد. برای ذخیره‌ی لاگ‌ها در یک فایل متنی بر روی سرور هاست، می‌توان از Serilog بهره برد که روش آن در اینجا  توضیح داده شده است. حال اگر بخواهیم این لاگ‌ها را در یک پایگاه داده ذخیره کنیم چطور؟

ایجاد کلاس لاگ

برای این منظور ابتدا کلاسی را برای ذخیره‌ی لاگ‌ها در پایگاه داده به شکل زیر ایجاد می‌نماییم:
public class DBLog
    {
        public int  DBLogId { get; set; }
        public string? LogLevel { get; set; }
        public string? EventName { get; set; }
        public string? Message { get; set; }
        public string? StackTrace { get; set; }
        public DateTime CreatedDate { get; set; }=DateTime.Now;
    }


ایجاد دیتابیس لاگر

کلاس DBLogger از اینترفیس ILogger ارث بری می‌کند و دارای سه متد است که مهمترین آنها متد Log می‌باشد که درحقیقت با هر بار فراخوانی Logger در برنامه فراخوانی می‌شود. برای مطالعه‌ی بیشتر در رابطه با دو متد دیگر می‌توانید به اینجا مراجعه نمایید.
public class DBLogger:ILogger

    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DBLogger(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }


        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = exception?.Message,
                StackTrace=exception?.StackTrace                
            }; 
            _dbContext.DBLogs.Add(dblLogItem);
            _dbContext.SaveChanges();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }
    }


ایجاد یک لاگ پروایدر سفارشی

حال باید یک لاگ پروایدر سفارشی را ایجاد کنیم تا بتوان یک نمونه از دیتابیس لاگر سفارشی بالا (DBLogger) را ایجاد کرد.

public class DbLoggerProvider:ILoggerProvider
    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new D‌BLogger(_dbContext);
        }

        public void Dispose()
        {
        }
    }  

همانطور که ملاحظه می‌نمایید، این لاگ پروایدر، از اینترفیس ILoggerProvider ارث بری کرده‌است که دارای متد CreateLogger می‌باشد ئ این متد با شروع برنامه، یک نمونه از دیتابیس لاگر سفارشی ما را ایجاد می‌کند. در سازنده‌ی این کلاس، DatabaseContext را مقدار دهی نموده‌ایم تا آنرا به کلاس DBLogger ارسال نماییم.

در انتها کافیست در کلاس Startup.cs این لاگ پروایدر سفارشی (DbLoggerProvider  ) را صدا بزنیم.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            .
            .
            .
            #region CustomLogProvider
            var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
            var appDbContext = serviceProvider.GetRequiredService<ApplicationDbContext>();
            loggerFactory.AddProvider(new DbLoggerProvider(appDbContext));
            #endregion
            .
            .
            .
در اینجا ابتدا DBContext پروژه را گرفته سپس DbLoggerProvider   را صدا زده و مقدار DBContext را به آن پاس می‌دهیم.


مشکل!

منطق کدهای بالا کاملا صحیح می‌باشد، اما با اجرای یک اپلیکیشن واقعی، در ابتدای کار اینقدر تعداد فراخوانی ثبت لاگ‌ها در پایگاه داده بالا می‌رود که اپلیکیشن هنگ می‌کند. برای حل این مشکل باید یک صف همزمانی برای ثبت لاگ‌ها تشکیل شود. برای این منظور من از این مطلب پروژه‌ی DNTIdentity بهره بردم. بنابراین باید پروایدر را به شکل زیر تصحیح کنیم:
public class DbLoggerProvider:ILoggerProvider
    {
        private readonly CancellationTokenSource _cancellationTokenSource = new();
        private readonly IList<DBLog> _currentBatch = new List<DBLog>();
        private readonly TimeSpan _interval = TimeSpan.FromSeconds(2);

        private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>());

        private readonly Task _outputTask;
        private readonly ApplicationDbContext _dbContext;
        private bool _isDisposed;

        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _outputTask = Task.Run(ProcessLogQueue);
        }
        public ILogger CreateLogger(string categoryName)
        {
            return new DBLogger(this,categoryName);
        }
        private async Task ProcessLogQueue()
        {
            while (!_cancellationTokenSource.IsCancellationRequested)
            {
                while (_messageQueue.TryTake(out var message))
                {
                    try
                    {
                        _currentBatch.Add(message);
                    }
                    catch
                    {
                        //cancellation token canceled or CompleteAdding called
                    }
                }

                await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token);
                _currentBatch.Clear();

                await Task.Delay(_interval, _cancellationTokenSource.Token);
            }
        }
        internal void AddLogItem(DBLog appLogItem)
        {
            if (!_messageQueue.IsAddingCompleted)
            {
                _messageQueue.Add(appLogItem, _cancellationTokenSource.Token);
            }
        }
        private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken)
        {
            try
            {
                if (!items.Any())
                {
                    return;
                }

                // We need a separate context for the logger to call its SaveChanges several times,
                // without using the current request's context and changing its internal state.
                foreach (var item in items)
                {
                    var addedEntry = _dbContext.DbLogs.Add(item);
                }

                await _dbContext.SaveChangesAsync(cancellationToken);

            }
            catch
            {
                // don't throw exceptions from logger
            }
        }

        [SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception",
            Justification = "don't throw exceptions from logger")]
        private void Stop()
        {
            _cancellationTokenSource.Cancel();
            _messageQueue.CompleteAdding();

            try
            {
                _outputTask.Wait(_interval);
            }
            catch
            {
                // don't throw exceptions from logger
            }
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        Stop();
                        _messageQueue.Dispose();
                        _cancellationTokenSource.Dispose();
                        _dbContext.Dispose();
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }  
همچنین دیتابیس لاگر نیز به شکل زیر تغییر خواهد کرد:
public class DBLogger:ILogger
    {
        private readonly LogLevel _minLevel;
        private readonly DbLoggerProvider _loggerProvider;
        private readonly string _categoryName;

        public DBLogger(
            DbLoggerProvider loggerProvider,
            string categoryName
        )
        {
            _loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider));
            _categoryName= categoryName;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return new NoopDisposable();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel >= _minLevel;
        }

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            var message = formatter(state, exception);

            if (exception != null)
            {
                message = $"{message}{Environment.NewLine}{exception}";
            }

            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = $"{_categoryName}{Environment.NewLine}{message}",
                StackTrace=exception?.StackTrace
            };
            _loggerProvider.AddLogItem(dblLogItem);
        }

      

        private class NoopDisposable : IDisposable
        {
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
            }
        }
    }  
خلاصه تغییرات اینگونه‌است که بجای ذخیره لاگ‌ها در دیتابیس در داخل کلاس DBLogger، آنها را در یک صف همزمانی اضافه می‌کنیم و پس از اتمام پروسه، ذخیره سازی لاگ‌ها را در لاگ پروایدر انجام می‌دهیم.
توسط ترکیب روش توضیح داده شده در این مقاله با مدیریت استثناءها در Blazor Server - قسمت دوم، علاوه برلاگ‌های معمولی، می‌توان تمامی استثناءهای یک اپلیکیشن را نیز به راحتی در پایگاه داده ذخیره نمود.
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 15 - نوشتن آزمون‌های واحد
یک نکته‌ی تکمیلی: روش کپی کردن فایل appsettings.json جهت دسترسی به آن در آزمون‌های واحد

ابتدا پس از build، فایل appsettings.json را در کنار خروجی برنامه کپی می‌کنیم (از پوشه‌ی وب، به پوشه‌ی bin\Debug\netcoreapp2.0 تست جاری)
<Target Name="CopyConfig" AfterTargets="AfterBuild">
    <Copy SourceFiles="..\MyWebApp\appsettings.json" DestinationFolder="$(OutDir)" />
</Target>
روش دیگر CopyToOutputDirectory زیر زمانیکه از SDK استفاده می‌شود، کار نمی‌کند:
  <ItemGroup>
    <None Update="..\MyWebApp\appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

سپس جائیکه ServiceCollection اولیه آزمون‌ها ساخته می‌شود، محل این فایل کپی شده را معرفی می‌کنیم:
var services = new ServiceCollection();
var configuration = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
   .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
   .AddInMemoryCollection(new[]
     {
       new KeyValuePair<string,string>("UseInMemoryDatabase", "true"),
     })
   .Build();
services.AddSingleton<IConfigurationRoot>(provider => configuration);
نظرات مطالب
تولید SiteMap استاندارد و ایجاد یک ActionResult اختصاصی برای Return کردن SiteMap تولید شده
سلام؛ عالی بود. من برای خواندن از بانک اطلاعاتی این کد رو نوشتم
 public virtual ActionResult Sitemap()
        {
            var data = new sunn.Models.ApplicationDbContext().Posts.ToList();
            SiteMap sm = new SiteMap();
            foreach (var siteno in data)
            {
                sm.Add(new Location()
                   {
                       Url = string.Format("http://www.MySite.ir/Develop/Home/Post/{0}", siteno.Id),
                       LastModified = siteno.InsertDate,
                       Priority = 0.5D
                   });
            }

            return new XmlResult(sm);
        }
زمانیکه در آدرس بار نام این اکشن رو میزنم، تمامی اطلاعات رو نشون میده؛ اما بدون هیچ فرمتی و پشت سر هم. اما ViewSource رو که میزنم توی source کاملا نقشه سایت رو نشون میده .

دو تا سوال : اول اینکه به چه نحوی میتونم آدرس فایل xml  را بدست بیارم که مثلاً به گوگل معرفی کنم و دوم اینکه به جای نمایش این صفحه ناخوانا بتوانم یک صفحه خوانا‌تر با فرمت نمایش بدم
مطالب
مدیریت استثناءها در Blazor Server - قسمت دوم
در قسمت اول دیدیم که توسط Error boundary می‌توان استثناءها را در Blazor مدیریت کرد؛ اما اگر بخواهیم قدری سفارشی‌تر عمل کرده و علاوه بر نمایش پیغام خطای مناسب به صورت جاوا اسکریپتی، استثنای رخ داده را لاگ کنیم چطور؟
خبر خوب اینکه این مهم نیز به راحتی امکان پذیر است؛ با استفاده از مفهوم CascadingValueها.
یک کامپوننت Error.razor به شکل زیر ایجاد می‌کنیم:
@using Microsoft.Extensions.Logging
@inject ILogger<Error> Logger
@inject IJSRuntime jsRuntime

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>

@code {
    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public void ProcessError(Exception ex)
    {
        Logger.LogError("Error:ProcessError - Type: {Type} Message: {Message}", 
            ex.GetType(), ex.Message);
        jsRuntime.ToastrError("متاسفانه خطایی رخ داد");
        //StateHasChanged();
    }
}
همانطور که مشخص است این کامپوننت سفارشی توسط متد ProcessError می‌تواند خطاها را با استفاده از logger توکار Blazor لاگ نموده و پیغام خطای جاوااسکریپتی نمایش دهد. بدیهی است که می‌توان از لاگ کننده‌های دیگری نظیر Serilog، Elmah و حتی لاگ کننده‌های سفارشی دیگر نیز برای لاگ کردن در این متد بهره جست.
از StateHasChanged زمانی استفاده خواهد شد که متد پردازش خطا می‌خواهد به صورت مستقیم در رندر شدن رابط کاربری کامپوننتی که در آن استثنایی رخ داده‌است، دخالت کند. برای مثال زمانیکه می‌خواهیم تغییری در عناصر رندر شده صفحه، بعد از خطا ایجاد کنیم (رنگ دکمه ای عوض شود یا رنگ فونت برچسب یا تکست باکس یا ...).
حال کامپوننت App.razor را به شکل زیر ویرایش می‌نماییم:
<Error>
    <Router ...>
        ...
    </Router>
</Error>
در حقیقت کامپوننت Router را توسط کامپوننت سفارشی خودمان (Error) محصور می‌کنیم تا کامپوننت Error به صورت آبشاری به هر کامپوننت برنامه که Error را به صورت [ CascadingParameter ]  درنظر بگیرد منتقل شود.
حال فقط کافی است برای پردازش خطاها به شکل زیر در کامپوننت‌های دیگر عمل نمود:
@code {
    [CascadingParameter]
    public Error? Error { get; set; }

    private void CreatePost()
    {
        try
        {           
           throw new InvalidOperationException("پست ساخته نشد!");            
        }
        catch (Exception ex)
        {
            Error?.ProcessError(ex);
        }
    }
}
همینطور که ملاحظه می‌نمایید کامپوننت Error به عنوان یک CascadingParameter تعریف شده‌است و در یک بلاک try catch متد ProcessError کامپوننت Error صدا زده شده و استثنای صادر شده به آن ارسال شده‌است. در مثال من کامپوننت Error فقط یک متد پردازش خطا دارد. بدیهی است که این کامپوننت می‌تواند چندین متد پردازش خطای سفارشی دیگر نیز برای مقاصد مختلف داشته باشد.