مطالب
C# 7 - Ref Returns and Ref Locals
C# 7 به همراه تغییرات قابل توجهی در مورد نحوه‌ی دریافت خروجی از متدها است که نمونه‌هایی از آن‌ها را مانند tuples و out variable، پیشتر بررسی کردیم. در ادامه تغییرات جدید دیگری را به نام ref locals و ref returns نیز بررسی خواهیم کرد و هدف از آن، کاهش تعداد بار کپی کردن مقادیر و یا اعمال dereferencing جهت بالا بردن کارآیی برنامه هستند.


انتقال توسط مقدار

اگر پارامتری به صورت value type تعریف شود، این مقدار درون متد، ایجاد شده و حافظه‌ای به آن اختصاص داده می‌شود و سپس در انتهای متد تخریب خواهد شد. بنابراین تغییری در مقدار آن، سبب انعکاس آن به فراخوان متد، نخواهد شد.
static void PassByValueSample()
{
    int a = 1;
    PassByValue(a);
    Console.WriteLine($"after the invocation of {nameof(PassByValue)}, {nameof(a)} = {a}");
}
 
static void PassByValue(int x)
{
    x = 2;
}
در این مثال متد PassByValue تنها یک کپی از مقدار متغیر a را دریافت می‌کند. بنابراین هر تغییری که درون متد PassByValue بر روی این مقدار دریافتی رخ دهد، به فراخوان آن منتقل نخواهد شد.


انتقال توسط ارجاع

برای بازگشت مقدار تغییر داده شده‌ی توسط یک متد، می‌توان یک نوع خروجی را برای آن تعریف کرد. راه دیگر آن تعریف یک پارامتر توسط واژه‌ی کلیدی ref است. پارامتری که به این روش تعریف شود، هم ارسال کننده‌ی مقدار و هم دریافت کننده‌ی تغییرات خواهد بود.
static void PassByReferenceSample()
{
    int a = 1;
    PassByReference(ref a);
    Console.WriteLine($"after the invocation of {nameof(PassByReference)}, {nameof(a)} = {a}");
}
 
static void PassByReference(ref int x)
{
    x = 2;
}
در این مثال متغیر x به مقدار متغیر a اشاره می‌کند. بنابراین هر تغییری که بر روی آن صورت گیرد، به متغیر a هم اعمال و منعکس خواهد شد.


متغیرهای out

با استفاده از واژه‌ی کلیدی ref، می‌توان یک مقدار را هم به تابع ارسال کرد و هم از آن دریافت نمود. اما اگر تنها قرار است مقداری از تابع بازگشت داده شود، می‌توان از متغیرهای out استفاده کرد.
static void OutSample()
{
    Out(out int a);
    Console.WriteLine($"after the invocation of {nameof(Out)}, {nameof(a)} = {a}");
}
 
static void Out(out int x)
{
    x = 2;
}
در حالت استفاده‌ی از واژه‌ی کلیدی out، نیازی به مقدار دهی اولیه‌ی متغیر ارسالی به متد نیست و در اینجا روش جدید تعریف متغیرهای out را در C# 7 نیز مشاهده می‌کنید که نیازی نیست تا ابتدا int a را تعریف کرد و سپس در متد Out آن‌را فراخوانی نمود. می‌توان کل عملیات را به صورت خلاصه در یک سطر ذکر کرد.
البته اگر تنها قرار است یک مقدار را از یک متد دریافت کنیم، بهتر است نوع خروجی متد را مشخص کرد و از آن استفاده نمود؛ بجای استفاده‌ی از متغیرهای out. یک نمونه کاربرد مفید متغیرهای out در خود فریم ورک و توسط متد TryParse وجود دارد:
if (int.TryParse("42", out var result))
{
   Console.WriteLine($"the result is {result}");
}


Ref Locals در C# 7

در C# 7، امکان تعریف متغیرهای محلی از نوع ref نیز وجود دارد:
 int x = 3;
ref int x1 = ref x;
x1 = 2;
Console.WriteLine($"local variable {nameof(x)} after the change: {x}");
در اینجا متغیر x1 دارای ارجاعی است به متغیر x. بنابراین تغییر مقدار x1، به متغیر x نیز منعکس خواهد شد.

باید دقت داشت که نمی‌توان یک مقدار را به یک ref variable نسبت داد:
 ref int i = sequence.Count();
به این ترتیب امکان اشتباه گرفتن بین value variables و reference variables توسط کامپایلر گوشزد خواهد شد:
 ref int number1 = null; // ERROR
ref int number2 = 42; // ERROR


Ref Returns در C# 7

در C# 7، واژه‌ی کلیدی ref را به همراه نوع خروجی نیز می‌توان بکار برد:
 static ref int ReturnByReference()
{
    int[] arr = { 1 };
    ref int x = ref arr[0];
    return ref x;
}
در این مثال ارجاعی به اولین عضو آرایه‌ی تعریف شده، به عنوان خروجی متد بازگشت داده می‌شود. همچنین می‌توان این کد را به صورت ساده‌تر ذیل نیز نوشت:
static ref int ReturnByReference2()
{
   int[] arr = { 1 };
   return ref arr[0];
}
باید دقت داشت که یک متغیر int محلی را نمی‌توان به صورت ref بازگشت داد. علت اینجا است که متغیر int یک value type است و در انتهای متد تخریب خواهد شد. بنابراین امکان بازگشت آن به صورت ref میسر نیست. اما آرایه‌ها reference type بوده و بر روی heap تشکیل می‌شوند. در این حالت متغیر int داخل آرایه را می‌توان به صورت ref بازگشت داد.

هرچند امکان بازگشت یک متغیر محلی int به صورت ref وجود ندارد، اما اگر این متغیر به صورت ref به تابع ارسال شده باشد، این امکان میسر است:
static ref int ReturnByReference3(ref int x)
{
    x = 2;
    return ref x;
}

چند نکته: امکان تعریف فیلد ref و یا خاصیت get;set دار از نوع ref وجود ندارد. اما تعریف خواصی که یک ref را بازگشت می‌دهند، میسر هستند:
class Thing1
{
    ref string _Text1; /* Error */
 
    ref string Text2 { get; set; } /* Error */
 
 
    string _text = "Text";
    ref string Text3 { get { return ref _text; } } // Properties that return a reference are allowed
}


مزیت کار با ref returns

ref return‌ها شاید آنچنان در کدهای روزمره‌ی #C بکارگرفته نشوند، اما نیاز به کدهای unsafe و کار مستقیم با pointers را به حداقل می‌رسانند و به آن می‌توان لقب safe pointer را اطلاق کرد؛ از این جهت که این کد هنوز هم یک managed code است و نه یک unsafe code.
private MyBigStruct[] array = new MyBigStruct[10];
private int current;
 
public ref MyBigStruct GetCurrentItem()
{
   return ref array[current];
}
مهم‌ترین مزیت این قابلیت جدید را در قطعه کد فوق ملاحظه می‌کنید. در طراحی بازی‌ها عموما استفاده‌ی از آرایه‌های بزرگ از پیش تخصیص داده شده‌ی structها بسیار مرسوم است (چون میزان مصرف حافظه‌ی کمتری را نسبت به نوع‌های ارجاعی دارند و فشار کمتری را به GC وارد می‌کنند). اکنون با معرفی این قابلیت، دیگر نیازی نیست تا مدام آرایه‌های بزرگی از structها را از قسمتی به قسمت دیگر کپی کرد و سپس بر روی عناصری از آن‌ها عملیاتی را انجام داد و مجددا این حاصل را به مکان مدنظر کپی کرد. در اینجا بدون کپی کردن value types می‌توان با ایجاد ارجاعی به آن‌ها، تغییرات مدنظر را به آن‌ها اعمال کرد.
مطالب
افزونه جی کوئری RowAdder
دیروز در یک برنامه میخواستم کاربر بتواند لیست مواد مصرفی یک کارخانه را ایجاد کند که نیاز بود کاربر بتواند از هر سطر به تعداد نامحدود ایجاد کند و برای انتخاب هر یک از مواد به همراه جزئیات آن یک سطر به لیست اضافه شود. برای اینکار میتوانیم با استفاده از فناوری جی کوئری اینکار را انجام دهیم ولی بهتر بود که این مورد به یک افزونه تبدیل میشد تا در دفعات بعدی بسیار راحت‌تر باشیم. جهت آشنایی با پلاگین نویسی بهتر هست این مقالات (+) را مطالعه فرمایید.

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

گام اول:
فایل‌های مورد نظر را بعد از صدا زدن کتابخانه‌ی جی کوئری صدا بزنید.
<link type="text/css" href="css/RowAdder.css" rel="stylesheet" />
    <script src="js/RowAdder.js" type="text/javascript"></script>


گام دوم :
 در تکه کدهای html، کدی را که قرار است در هر سطر تکرار شود، داخل یک div قرار داده و نامی مثل row-sample را برای آن قرار دهید (فعلا حتما این نام باشد)، بعدها پلاگین، کدهای داخل این تگ div را به عنوان هر سطر خواهد شناخت:
<div id="row-sample">
    <form style="margin: 0; padding: 0;">
        Name:<input type="text"/>
        <input type="radio" name="Gender" value="male" checked="checked">Male
        <input type="radio" name="Gender" value="female">Female
    </form>
</div>


گام سوم:
 سپس یک div دیگر ایجاد کنید و نامی مثل mypanel را به آن بدهید تا سطرهایی که ایجاد می‌شوند داخل این div قرار بگیرند.
<div id="mypanel"></div>

گام چهارم:
در بخش head یک تگ اسکریپت باز کرده و کدهای زیر را به آن اضافه می‌کنیم. این کد باعث می‌شود که پلاگین فعال شود.
<script>
$(document).ready(function() {
$("#mypanel").RowAdder();
});
</script>
گام پنجم:
 یک دکمه جهت افزودن سطر به صفحه اضافه می‌کنیم
<button id="addanotherform">Add New Form</button>

و در قسمت تگ اسکریپت هم کد زیر را اضافه می‌کنیم:
$("#addanotherform").on('click', function() {
                $("#mypanel").RowAdder('add');
            });

حال از صفحه تست می‌گیریم: با هر بار کلیک بر روی دکمه‌ی Add New Form یک سطر جدید ایجاد می‌گردد.


در تصویر بالا دکمه‌های دیگر هم دیده می‌شوند که به دیگر متدهای آن اشاره دارد:

جهت مخفی سازی:
 $("#mypanel").RowAdder('hide');

چهت نمایش:
$("#mypanel").RowAdder('show');

جهت افزودن سطر با کد:
$("#mypanel").RowAdder('add');

جهت دریافت تعداد سطرهای ایجاد شده:
$("#mypanel").RowAdder('count')


جهت دریافت کدهای یک سطر در اندیس x

$("#mypanel").RowAdder('content', 3)

جهت حذف یک سطر با اندیس x
$("#mypanel").RowAdder('remove', 3);

همانطور که با صدا زدن اولین متد پلاگین متوجه شدید و نتیجه‌ی آن را در دمو دیدید، این پلاگین از پیش فرض‌هایی جهت راه اندازی اولیه استفاده می‌کند که این پیش فرض‌ها عبارتند از تگ row-sample که بدون معرفی رسمی، آن را شناسایی کرد. همچنین ممکن است بخواهید عبارت Remove را با کلمه‌ی فارسی «حذف» جایگزین نمایید. برای اینکار می‌توانید پلاگین را به شکل زیر به کار ببرید:
    $("#mypanel").RowAdder({
                sample: '#my-custom-sample',
                type: 'text',
                value:'حذف'
        });

تغییر اولین پیش فرض، تغییر نام تگ row-sample به my-custom-sample بود و در مرحله‌ی بعد هم نام فارسی حذف را جایگزین remove کردیم. عبارت type به طور پیش فرض بر روی text قرار دارد که اجباری به ذکر آن در کد بالا نبود. ولی اگر دوست دارید که به جای نمایش عبارت حذف، از یک آیکن یا تصویر استفاده کنید، کد را به شکل زیر تغییر دهید:
  $("#mypanel").RowAdder({
                type: 'image',
                value: 'images/remove.png'
            });
در خطوط بالا عبارت type با image مقدار دهی شد و به پلاگین می‌گوید که به جای متن، از تصویر استفاده کن. همچنین value را به جای متن با آدرس تصویر مقداردهی کرده‌ایم و نتیجه را می‌توانید در دموی قرار گرفته در گیت هاب ببینید.

فایل RowAdder.css
در بردارنده هر سطر
.each-section {
    margin: 20px;
    padding: 5px;
}

جهت استایل بندی لینک چه تصویر و چه متن
.remove-link {
    color:#999;
     text-decoration: none;
}

a:hover.remove-link {
   color:#802727;
}
جهت تغییر استایل بر روی خود تصویر
.remove-image {
    
}

آشنایی با کد پلاگین
(function ($) {
    
    var settings = null;
  $.fn.RowAdder = function (method) {
    
            // call methods
            if (methods[method]) {
                return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
            } else if (typeof method === 'object' || !method) {
                return methods.init.apply(this, arguments);
            } else {
                $.error('Method ' + method + ' does not exist on jQuery.RowAdder');
            }
      

    };
})(jQuery);
در قسمت دوم آموزش پلاگین نویسی برای جی کوئری، متدها به طور واضح توضیح داده شده‌اند. این کدها وظیفه دارند متدهایی را که کاربر درخواست داده است، شناسایی و به همراه آرگومان‌های آن به سمت توابعی که به هر نام متد اختصاص داده‌ایم، ارسال کنند. در صورتیکه متدی با آرگومان‌های ناهماهنگی ارسال شوند، پیام خطایی ارسال می‌گردد و در صورتیکه تعریف نشود، به طور مستقیم init را صدا می‌زند. متغیر settings هم بعدا با تنظیمات پیش فرض پر می‌شود.

متدها
//methods
    var methods = {
        init: function (options) {
            //default-settings
             settings = $.extend({
                'sample': '#row-sample',
                'type': 'text',
                'value': 'Remove'
             }, options);
             this.attr('data-sample', settings.sample);
             this.attr('data-type', settings.type);
             this.attr('data-value', settings.value);
            Do(this);
        },
        show: function () {
            this.css("display", "inline");
        },
        hide: function () {
            this.css("display", "none");
        },
        add: function () {
            Do(this);
        },
        remove: function (index) {
            console.log(index);
           this.find(".each-section")[index].remove();
        },
        content: function (index) {
            return this.find(".each-section")[index];
        },
        count: function (index) {
            return this.find(".each-section").size();
        }
    };
متد init تنظیمات پیش فرض را دریافت می‌نماید و سپس بر روی المانی که پلاگین روی آن واقع شده‌است، مقادیر را ذخیره می‌کند تا در آینده با صدا زدن متدهای دیگر آن را استفاده نماید. کلمه‌ی this در واقع به تگی اشاره می‌کند که پلاگین روی آن اعمال شده است که در مثال‌های بالا mypanel نام داشت. متد Do تابع اصلی ما را در بر دارد که کدهای اصلی پلاگین را شامل می‌شود. مابقی متدها در واقع  جست و جویی بر المان‌ها هستند.

تابع Do
    function Do(panelDiv) {

        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });
        //remove event for remove selected form

        //create new form
        sectionDiv.html(sample);

        link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });

        if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

        //add new created form on document
        panelDiv.append(sectionDiv);
       
    }
آرگومان داده شده، در واقع همان this هست که به این تابع ارسال شده است. در اولین گام تنظیمات ذخیره شده را که قبلا ذخیره کرده‌ایم، واکشی می‌کنیم. سپس تگ row-sample یا هر نامی را که به آن اختصاص داده شده است، می‌یابیم و محتوای آن را به شکل html در قالب string بیرون می‌کشیم. این کد html در واقع نمونه‌ای است که قرار است در سطر تکرار شود. البته تگ نمونه فقط برای نمونه به کار می‌رود و نیازی نیست روی صفحه نمایش داده شود؛ پس آن را مخفی می‌کنیم. از آنجا که ممکن است این سطری که ایجاد می‌شود، سطر اول نباشد و قبلا هم سطرهایی توسط همین متد ایجاد شده‌اند، بررسی می‌کنیم چند تگ با کلاس each-section داریم. اگر بیشتر از صفر باشد یعنی قبلا سطرهایی ایجاد شده است. در غیر اینصورت این اولین سطر ماست. اولین سطر توسط init صدا زده می‌شود و مابقی توسط متد add انجام می‌گیرد.
        settings.sample = panelDiv.data('sample');
        settings.type = panelDiv.data('type');
        settings.value = panelDiv.data('value');
        //find sample code
        var rowsample = $(settings.sample);
        rowsample.css("display", "none");
        var sample = rowsample.html();


        var i = panelDiv.find(".each-section").size();
در خطوط بعدی یک سری متغیر داریم که برای هر کدام یک قالب تگ div با کلاس‌های مختلف می‌سازیم. sectionDiv یک تگ  div  با کلاس each-section است که هر سطر را به طور کامل در خود قرار می‌دهد. link، جهت ساخت لینک حذف با کلاس remove-link به کار می‌رود. image هم یک تگ image می‌سازد تا اگر کاربر درخواست 'type:'image را داد، به جای لینک متنی حذف، از تصویر استفاده شود.
        //add html details to create a correct template
        var sectionDiv = $('<div />', { "class": 'each-section', 'id': 'section'+i });
        var image = $("<img />", { "src": settings.value,"class":"remove-image" });
        var link = $("<a />", { "text": settings.value,"class":"remove-link" });

در خط بعدی محتویات نمونه را داخل تگ sectiondiv قرار می‌دهیم:
//create new form
        sectionDiv.html(sample);

بعد از آن برای رویداد کلیک لینک حذف، کد زیر را وارد می‌کنیم:
   link.on('click', function (e) {

            e.preventDefault();
            var $this = $(this);
            $this.closest(".each-section").remove();
        });
متد closest در جی کوئری این وظیفه را دارد تا به سمت تگ‌های والد تگ this حرکت کند و با برخوردن با اولین تگ والد با کلاس each-section، آن تگ والد را بازگرداند و سپس متد remove را روی آن اجرا کند تا آن تگ به همراه تمام فرزندانش حذف شوند.

اولین شرط زیر بررسی می‌کند که آیا این سطری که ایجاد شده است سطر دوم به بعد است یا خیر؟ اگر آری پس باید دکمه‌ی حذف را به همراه داشته باشد. در صورتیکه سطر دوم به بعد باشد، وارد آن می‌شود. حالا بررسی می‌کند که کاربر برای دکمه‌ی حذف، درخواست لینک تصویری یا لینک متنی داده است و لینک مناسب را ساخته و آن را به انتهای sectionDiv اضافه می‌کند.
   if (i > 0) {
            if (settings.type == 'image') {
                link.text('');
                link.append(image);

            }
            sectionDiv.append(link);
        }

در انتها کل تگ sectionDiv را به تگ داده شده اضافه می‌کنیم تا به کاربر نمایش داده شود.
//add new created form on document
        panelDiv.append(sectionDiv);
مطالب
بررسی Source Generators در #C - قسمت سوم - بهبود کارآیی برنامه با تبدیل عملیات Reflection به تولید کد خودکار
همانطور که در قسمت اول این سری نیز عنوان شد، انجام عملیات Reflection عموما به همراه سربار محاسبه‌ی هرباره‌ی اطلاعات مورد نیاز آن است و اکنون می‌توان یک چنین محاسباتی را توسط Source generators، در زمان کامپایل برنامه، تامین و جزئی از خروجی نهایی کامپل شده‌ی آن کرد تا کارآیی برنامه به شدت افزایش یابد. یک نمونه مثال آن، استفاده از ویژگی Display بر روی عناصر یک enum است تا بتوان توضیحات بیشتری را جهت نمایش در UI، ارائه داد:
using System.ComponentModel.DataAnnotations;

namespace NotifyPropertyChangedGenerator.Demo;

public enum Gender
{
    NotSpecified,
    [Display(Name = "مرد")] Male,
    [Display(Name = "زن")] Female
}
روش متداول جهت دسترسی به اطلاعات ویژگی Display، استفاده از Reflection به صورت زیر است:
public static class Extensions
{
    public static string GetDisplayName(this Enum value)
    {
        if (value is null)
            throw new ArgumentNullException(nameof(value));

        var attribute = value.GetType().GetField(value.ToString())?
            .GetCustomAttributes<DisplayAttribute>(false).FirstOrDefault();

        if (attribute is null)
            return value.ToString();

        return attribute.GetType().GetProperty("Name")?.GetValue(attribute, null)?.ToString();
    }
}
یعنی هرجائی که در برنامه نیاز باشد تا برای مثال نام نمایشی Gender.Female محاسبه شود، باید یکبار عملیات فوق در زمان اجرا، تکرار گردد با محاسبه‌ی تمام ویژگی‌های یک عنصر enum، بررسی وجود DisplayAttribute در این بین و در صورت وجود، محاسبه‌ی مقدار خاصیت Name آن.
یعنی در اصل متد کمکی که برای اینکار نیاز داریم، چنین خروجی را دارد:
namespace NotifyPropertyChangedGenerator.Demo
{
  public static class GenderExtensions
  {
      public static string GetDisplayName(this Gender @enum)
      {
          return @enum switch
            {
                Gender.NotSpecified => "NotSpecified",
                Gender.Male => "مرد",
                Gender.Female => "زن",
                _ => throw new ArgumentOutOfRangeException(nameof(@enum))
            };
      }
  }
}
مزیت این روش نسبت به Reflection، از پیش محاسبه شده بودن و سرعت بالای کار با آن است؛ اما ... باید به ازای هر enum نوشته شده، یکبار به صورت اختصاصی، تکرار شود و همچنین اگر اطلاعات enum متناظر با آن تغییر کرد، نیاز است تا این کلاس‌ها و متدهای کمکی نیز اصلاح شوند. به همین جهت است که عموما کار با Reflection ترجیح داده می‌شود؛ چون حجم کدنویسی کمتری را به همراه دارد و همچنین می‌تواند انواع و اقسام enum را پوشش دهد و عمومی است.
با ارائه‌ی Source Generators، مشکلات یاد شده دیگر وجود ندارند. یعنی کار تولید متدهای اختصاصی برای هر enum، خودکار است و همچنین به روز رسانی آنی آن‌ها با هر تغییری در enum‌ها نیز پیش‌بینی شده‌است.


تهیه‌ی تولید کننده‌ی خودکار کدی که نام نمایشی enumها را به صورت از پیش محاسبه شده ارائه می‌دهد

در قسمت قبل، با روش تهیه و استفاده از Source Generators آشنا شدیم. در اینجا نیز از همان قالب، در جهت تولید کد متد الحاقی GetDisplayName فوق، استفاده خواهیم کرد. یعنی هدف رسیدن به کلاس GenderExtensions فوق و متد GetDisplayName آن، در زمان کامپایل برنامه و به صورت خودکار است:
[Generator]
public class EnumExtensionsGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {}

    public void Execute(GeneratorExecutionContext context)
    {
        var compilation = context.Compilation;
        foreach (var syntaxTree in compilation.SyntaxTrees)
        {
            var semanticModel = compilation.GetSemanticModel(syntaxTree);
            var immutableHashSet = syntaxTree.GetRoot()
                .DescendantNodesAndSelf()
                .OfType<EnumDeclarationSyntax>()
                .Select(enumDeclarationSyntax => semanticModel.GetDeclaredSymbol(enumDeclarationSyntax))
                .OfType<ITypeSymbol>()
                /*.Where(typeSymbol => typeSymbol.GetAttributes().Any(
                    attributeData => string.Equals(attributeData.AttributeClass?.Name, "GenerateExtensions",
                        StringComparison.Ordinal)
                ))*/
                .ToImmutableHashSet();

            foreach (var typeSymbol in immutableHashSet)
            {
                var source = GenerateEnumExtensions(typeSymbol);
                context.AddSource($"{typeSymbol.Name}Extensions.cs", source);
            }
        }
    }
کار را با ایجاد یک کلاس عمومی جدید که پیاده سازی کننده‌ی اینترفیس ISourceGenerator و مزین به ویژگی Generator است، شروع می‌کنیم. در مورد وابستگی‌های مورد نیاز یک چنین پروژه‌ای، در قسمت قبل توضیحات کافی ارائه شد.
در اینجا در متد Execute، دسترسی کاملی را به اطلاعات تهیه شده‌ی توسط کامپایلر داریم. توسط آن تمام Enumهای برنامه را یا همان EnumDeclarationSyntax را در اینجا، یافته و سپس حلقه‌ای را بر روی اطلاعات آن‌ها تشکیل داده و برای تک تک آن‌ها، توسط متد GenerateEnumExtensions، کد معادل کلاس GenderExtensions را که در این مطلب معرفی شد، تولید می‌کنیم. در پایان کار نیز این کد را توسط متد AddSource، به کامپایلر معرفی خواهیم کرد تا بلافاصله در IDE ظاهر شده و قابلیت استفاده را پیدا کند.

یک نکته: اگر می‌خواهید صرفا enumهای خاصی در این بین بررسی شوند، می‌توانید کدهای یک Attribute سفارشی را مثلا با نام فرضی [GenerateExtensions] در همینجا توسط متد context.AddSource به مجموعه سورس‌ها اضافه کنید و سپس بر اساس نام آن، در قسمت Where ایی که کامنت شده‌است، تنها اطلاعات مدنظر را فیلتر و پردازش کنید.

متدی هم که ابتدا کلاس Extensions را بر اساس نام هر Enum موجود تولید و سپس بدنه‌ی متد GetDisplayName اختصاصی آن‌را تکمیل می‌کند، به صورت زیر است:
    private string GenerateEnumExtensions(ITypeSymbol typeSymbol)
    {
        return $@"namespace {typeSymbol.ContainingNamespace}
{{
  public static class {typeSymbol.Name}Extensions
  {{
      public static string GetDisplayName(this {typeSymbol.Name} @enum)
      {{
          {GenerateExtensionMethodBody(typeSymbol)}
      }}
  }}
}}";
    }

    private static string GenerateExtensionMethodBody(ITypeSymbol typeSymbol)
    {
        var sb = new StringBuilder();
        sb.Append(@"return @enum switch
            {
");

        foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>())
        {
            var displayAttribute = fieldSymbol.GetAttributes()
                .FirstOrDefault(attributeData =>
                    string.Equals(attributeData.AttributeClass?.Name, "DisplayAttribute", StringComparison.Ordinal));
            if (displayAttribute is null)
            {                
                sb.AppendLine(
                    $@"                {typeSymbol.Name}.{fieldSymbol.Name} => ""{fieldSymbol.Name}"",");
            }
            else
            {
                var displayAttributeName = displayAttribute.NamedArguments
                    .FirstOrDefault(x => string.Equals(x.Key, "Name", StringComparison.Ordinal))
                    .Value;
                sb.AppendLine(
                    $@"                {typeSymbol.Name}.{fieldSymbol.Name} => ""{displayAttributeName.Value}"",");
            }
        }

        sb.Append(
            @"                _ => throw new ArgumentOutOfRangeException(nameof(@enum))
            };");

        return sb.ToString();
    }
در مورد روش استفاده‌ی از این source generator نیز در قسمت قبل بحث شد و فقط کافی است ارجاعی را به اسمبلی آن به پروژه‌ی مدنظر افزود و OutputItemType را به آنالایزر تنظیم کرد.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: SourceGeneratorTests-part3.zip


سؤال: چگونه می‌توان کدهای تولید شده‌ی توسط یک Source Generator را ذخیره کرد؟

Source Generators به صورت پیش‌فرض هیچ فایلی را بر روی دیسک سخت ذخیره نمی‌کنند و تمام عملیات آن‌ها در حافظه انجام می‌شود. اگر علاقمند به مطالعه‌ی این خروجی‌های خودکار، به صورت فایل‌های واقعی هستید، نیاز به انجام تغییرات زیر در فایل csproj پروژه‌ی مصرف کننده‌ی Source Generator است:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
  </PropertyGroup>
  
  <Target Name="CleanSourceGeneratedFiles" BeforeTargets="BeforeBuild" DependsOnTargets="$(BeforeBuildDependsOn)">
    <RemoveDir Directories="$(CompilerGeneratedFilesOutputPath)" />
  </Target>  
  <ItemGroup>
    <!-- Exclude the output of source generators from the compilation -->
    <Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" />
<Content Include="$(CompilerGeneratedFilesOutputPath)/**" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj" 
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>
توضیحات:
- EmitCompilerGeneratedFiles سبب ثبت فایل‌های خودکار تولید شده، بر روی دیسک سخت می‌شود که قالب مسیر پیش‌فرض ذخیره سازی آن به صورت زیر است:
{BaseIntermediateOutpath}/generated/{Assembly}/{SourceGeneratorName}/{GeneratedFile}
- اگر می‌خواهید نام پوشه‌ی generated را تغییر دهید، می‌توان از ویژگی CompilerGeneratedFilesOutputPath استفاده کرد.
- چون این فایل‌های cs. جدید ثبت شده‌ی بر روی دیسک سخت، مجددا وارد پروسه‌ی کامپایل می‌شوند و خود Source Generator هم یک نمونه‌ی از آن‌ها‌را پیش‌تر به کامپایلر معرفی کرده‌است، برنامه دیگر به علت وجود اطلاعات تکراری، کامپایل نخواهد شد. به همین جهت نیاز است تا قسمت Compile Remove فوق را نیز معرفی کرد تا کامپایلر از پوشه‌ی Generated تنظیمی، صرفنظر کند.
- اطلاعات موجود در پوشه‌ی Generated، فقط یکبار تولید می‌شوند؛ صرفنظر از اطلاعات موجود در حافظه که همیشه به روز است. به همین جهت اگر می‌خواهید نمونه‌های به روز شده‌ی آن‌ها را نیز بر روی دیسک سخت داشته باشید، نیاز به قسمت RemoveDir تنظیمی وجود دارد.
مطالب
EF Code First #8

ادامه بحث بررسی جزئیات نحوه نگاشت کلاس‌ها به جداول، توسط EF Code first


استفاده از Viewهای SQL Server در EF Code first

از Viewها عموما همانند یک جدول فقط خواندنی استفاده می‌شود. بنابراین نحوه نگاشت اطلاعات یک کلاس به یک View دقیقا همانند نحوه نگاشت اطلاعات یک کلاس به یک جدول است و تمام نکاتی که تا کنون بررسی شدند، در اینجا نیز صادق است. اما ...
الف) بر اساس تنظیمات توکار EF Code first، نام مفرد کلاس‌ها، حین نگاشت به جداول، تبدیل به اسم جمع می‌شوند. بنابراین اگر View ما در سمت بانک اطلاعاتی چنین تعریفی دارد:
Create VIEW EmployeesView
AS
SELECT id,
FirstName
FROM Employees

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

using System.ComponentModel.DataAnnotations;

namespace EF_Sample04.Models
{
[Table("EmployeesView")]
public class EmployeesView
{
public int Id { set; get; }
public string FirstName { set; get; }
}
}

در اینجا به کمک ویژگی Table، نام دقیق این View را در بانک اطلاعاتی مشخص کرده‌ایم. به این ترتیب تنظیمات توکار EF بازنویسی خواهد شد و دیگر به دنبال EmployeesViews نخواهد گشت؛ یا جدول متناظر با آن‌را به صورت خودکار ایجاد نخواهد کرد.
ب) View شما نیاز است دارای یک فیلد Primary key نیز باشد.
ج) اگر از مهاجرت خودکار توسط MigrateDatabaseToLatestVersion استفاده کنیم، پیغام خطای زیر را دریافت خواهیم کرد:

There is already an object named 'EmployeesView' in the database.

علت این است که هنوز جدول dbo.__MigrationHistory از وجود آن مطلع نشده است، زیرا یک View، خارج از برنامه و در سمت بانک اطلاعاتی اضافه می‌شود.
برای حل این مشکل می‌توان همانطور که در قسمت‌های قبل نیز عنوان شد، EF را طوری تنظیم کرد تا کاری با بانک اطلاعاتی نداشته باشد:

Database.SetInitializer<Sample04Context>(null);

به این ترتیب EmployeesView در همین لحظه قابل استفاده است.
و یا به حالت امن مهاجرت دستی سوئیچ کنید:
Add-Migration Init -IgnoreChanges
Update-Database

پارامتر IgnoreChanges سبب می‌شود تا متدهای Up و Down کلاس مهاجرت تولید شده، خالی باشد. یعنی زمانیکه دستور Update-Database انجام می‌شود، نه Viewایی دراپ خواهد شد و نه جدول اضافه‌ای ایجاد می‌گردد. فقط جدول dbo.__MigrationHistory به روز می‌شود که هدف اصلی ما نیز همین است.
همچنین در این حالت کنترل کاملی بر روی کلاس‌های Up و Down وجود دارد. می‌توان CreateTable اضافی را به سادگی از این کلاس‌ها حذف کرد.

ضمن اینکه باید دقت داشت یکی از اهداف کار با ORMs، فراهم شدن امکان استفاده از بانک‌های اطلاعاتی مختلف، بدون اعمال تغییری در کدهای برنامه می‌باشد (فقط تغییر کانکشن استرینگ، به علاوه تعیین Provider جدید، باید جهت این مهاجرت کفایت کند). بنابراین اگر از View استفاده می‌کنید، این برنامه به SQL Server گره خواهد خورد و دیگر از سایر بانک‌های اطلاعاتی که از این مفهوم پشتیبانی نمی‌کنند، نمی‌توان به سادگی استفاده کرد.



استفاده از فیلدهای XML اس کیوال سرور

در حال حاضر پشتیبانی توکاری توسط EF Code first از فیلدهای ویژه XML اس کیوال سرور وجود ندارد؛ اما استفاده از آن‌ها با رعایت چند نکته ساده، به نحو زیر است:

using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;

namespace EF_Sample04.Models
{
public class MyXMLTable
{
public int Id { get; set; }

[Column(TypeName = "xml")]
public string XmlValue { get; set; }

[NotMapped]
public XElement XmlValueWrapper
{
get { return XElement.Parse(XmlValue); }
set { XmlValue = value.ToString(); }
}
}
}


در اینجا توسط TypeName ویژگی Column، نوع توکار xml مشخص شده است. این فیلد در طرف کدهای کلاس‌های برنامه، به صورت string تعریف می‌شود. سپس اگر نیاز بود به این خاصیت توسط LINQ to XML دسترسی یافت، می‌توان یک فیلد محاسباتی را همانند خاصیت XmlValueWrapper فوق تعریف کرد. نکته‌ دیگری را که باید به آن دقت داشت، استفاده از ویژگی NotMapped می‌باشد، تا EF سعی نکند خاصیتی از نوع XElement را (یک CLR Property) به بانک اطلاعاتی نگاشت کند.

و همچنین اگر علاقمند هستید که این قابلیت به صورت توکار اضافه شود، می‌توانید اینجا رای دهید!



نحوه تعریف Composite keys در EF Code first

کلاس نوع فعالیت زیر را درنظر بگیرید:

namespace EF_Sample04.Models
{
public class ActivityType
{
public int UserId { set; get; }
public int ActivityID { get; set; }
}
}

در جدول متناظر با این کلاس، نباید دو رکورد تکراری حاوی شماره کاربری و شماره فعالیت یکسانی باهم وجود داشته باشند. بنابراین بهتر است بر روی این دو فیلد، یک کلید ترکیبی تعریف کرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
public class ActivityTypeConfig : EntityTypeConfiguration<ActivityType>
{
public ActivityTypeConfig()
{
this.HasKey(x => new { x.ActivityID, x.UserId });
}
}
}

در اینجا نحوه معرفی بیش از یک کلید را در متد HasKey ملاحظه می‌کنید.

یک نکته:
اینبار اگر سعی کنیم مثلا از متد db.ActivityTypes.Find با یک پارامتر استفاده کنیم، پیغام خطای «The number of primary key values passed must match number of primary key values defined on the entity» را دریافت خواهیم کرد. برای رفع آن باید هر دو کلید، در این متد قید شوند:

var activity1 = db.ActivityTypes.Find(4, 1);

ترتیب آن‌ها هم بر اساس ترتیبی که در کلاس ActivityTypeConfig، ذکر شده است، مشخص می‌گردد. بنابراین در این مثال، اولین پارامتر متد Find، به ActivityID اشاره می‌کند و دومین پارامتر به UserId.


بررسی نحوه تعریف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)

سناریوهای کاربردی بسیاری را جهت جداول خود ارجاع دهنده می‌توان متصور شد و عموما تمام آن‌ها برای مدل سازی اطلاعات چند سطحی کاربرد دارند. برای مثال یک کارمند را درنظر بگیرید. مدیر این شخص هم یک کارمند است. مسئول این مدیر هم یک کارمند است و الی آخر. نمونه دیگر آن، طراحی منوهای چند سطحی هستند و یا یک مشتری را درنظر بگیرید. مشتری دیگری که توسط این مشتری معرفی شده است نیز یک مشتری است. این مشتری نیز می‌تواند یک مشتری دیگر را به شما معرفی کند و این سلسله مراتب به همین ترتیب می‌تواند ادامه پیدا کند.
در طراحی بانک‌های اطلاعاتی، برای ایجاد یک چنین جداولی، یک کلید خارجی را که به کلید اصلی همان جدول اشاره می‌کند، ایجاد خواهند کرد؛ اما در EF Code first چطور؟

using System.Collections.Generic;

namespace EF_Sample04.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }

//public int? ManagerID { get; set; }
public virtual Employee Manager { get; set; }
}
}

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

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
public class EmployeeConfig : EntityTypeConfiguration<Employee>
{
public EmployeeConfig()
{
this.HasOptional(x => x.Manager)
.WithMany()
//.HasForeignKey(x => x.ManagerID)
.WillCascadeOnDelete(false);
}
}
}

با توجه به اینکه یک کارمند می‌تواند مسئولی نداشته باشد (خودش مدیر ارشد است)، به کمک متد HasOptional مشخص کرده‌ایم که فیلد Manager_Id را که می‌خواهی به این کلاس اضافه کنی باید نال پذیر باشد. توسط متد WithMany طرف دیگر رابطه مشخص شده است.
اگر نیاز بود فیلد Manager_Id اضافه شده نام دیگری داشته باشد، یک خاصیت nullable مانند ManagerID را که در کلاس Employee مشاهده می‌کنید،‌ اضافه نمائید. سپس در طرف تعاریف نگاشت‌ها به کمک متد HasForeignKey، باید صریحا عنوان کرد که این خاصیت، همان کلید خارجی است. از این نکته در سایر حالات تعاریف نگاشت‌ها نیز می‌توان استفاده کرد، خصوصا اگر از یک بانک اطلاعاتی موجود قرار است استفاده شود و از نام‌های دیگری بجز نام‌های پیش فرض EF استفاده کرده است.




مثال‌های این سری رو از این آدرس هم می‌تونید دریافت کنید: (^)

مطالب
مسیریابی در Angular - قسمت هفتم - بهبودهای بصری
در این قسمت ویژگی‌های بصری را مانند مشخص سازی مسیر انتخاب شده، در منوی سایت و همچنین نمایش «لطفا منتظر بمانید» را در حین نمایش قسمت‌هایی که با تاخیر از سرور دریافت می‌شوند، پیاده سازی خواهیم کرد.


تزئین مسیر انتخاب شده در منوی سایت

برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کرده‌است. این مورد شامل سلسه مراتب مسیریابی‌ها نیز می‌شود؛ برای مثال فعالسازی حالت انتخاب شده‌ی منوی سایت، به همراه برگه‌ی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژه‌ای به نام routerLinkActive تدارک دیده شده‌است. این دایرکتیو را می‌توان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آن‌را نیز می‌توان به یکی از کلاس‌های CSS برنامه مانند کلاس active تعریف شده‌ی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه می‌کند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
<div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
            </a>
</div>
در اینجا دایرکتیو‌های routerLinkActive، به هر کدام از لینک‌های تعریف شده اضافه گردیده‌اند. مقدار active در اینجا، به کلاس active بوت استرپ اشاره می‌کند. یا حتی می‌توان تعدادی کلاس جدا شده‌ی با کاما را نیز در اینجا ذکر کرد.

یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.


همانطور که مشاهده می‌کنید، همین دو تنظیم ساده سبب مشخص شدن برگه‌ی انتخابی شده‌اند.

منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرنده‌ی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
    <ul class="nav navbar-nav">
      <li routerLinkActive="active">
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products']">Product List</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products', 0, 'edit']">Add Product</a>
       </li>      
    </ul>
اینبار routerLinkActive به المان‌های li اعمال شده‌است؛ چون این المان‌های لیست، شیوه نامه‌ی المان‌های anchor را بازنویسی می‌کنند و اگر routerLinkActive را به لینک‌ها اعمال می‌کردیم، تغییری مشاهده نمی‌شد.


همانطور که مشاهده می‌کنید، در این حالت انتخاب منوی نمایش لیست محصولات، سبب تزئین آن به حالت انتخاب شده نیز گردیده‌است.

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


اینبار هر دو گزینه با هم انتخاب شده‌اند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کرده‌است. برای مدیریت میدان دید آن می‌توان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
      <li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
        <a [routerLink]="['/products']">Product List</a>
      </li>
routerLinkActiveOptions را تنها به ریشه‌ی مسیر products اعمال کرده‌ایم؛ چون این مسیر است که می‌تواند با تمام مسیرهای مشتق شده‌ی از آن نیز تطابق داشته باشد. تنظیم exact: true آن سبب خواهد شد تا تطابق با مسیرهای مشتق شده‌ی از آن ندید گرفته شوند.


اکنون کاربران بهتر می‌توانند درک کنند در کجای برنامه قرار دارند.


افزودن آیکن خطا به برگه‌ای که دارای مشکل اعتبارسنجی است

در ادامه می‌خواهیم اگر برگه‌ای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران ساده‌تر خواهد شد و به سادگی می‌توانند برگه‌های مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر می‌گرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگه‌ها استفاده خواهیم کرد.
        <div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span>
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span>
            </a>
        </div>
در اینجا دو span را تعریف کرده‌ایم که با کمک دایرکتیو ngClass سبب نمایش آیکن exclamation-sign در صورت وجود یک خطای اعتبارسنجی می‌شوند. به عبارتی اگر برگه‌ای معتبر نباشد، سبب درج کلاس آن در span جاری می‌شود:



رخ‌دادهای مسیریابی

هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش می‌کند، مسیریاب تعدادی رخ‌داد را نیز تولید خواهد کرد. از این رخ‌دادها جهت تحت نظر قرار دادن، عیب‌یابی و یا اجرای منطقی می‌توان استفاده کرد. این رخ‌دادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ می‌دهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المان‌های تعریف شده‌ی در تنظیمات مسیریابی رخ می‌دهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ می‌دهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظ‌های مسیرها و یا هدایت به یک جهت دیگر رخ می‌دهد.
- NavigationError، با شکست پیمایش یک مسیر رخ می‌دهد.

این رخ‌دادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال می‌شوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
@NgModule({
  imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
پس از این تغییر، اگر به developer tools مرورگر دقت کنید، یک چنین خروجی را می‌توان مشاهده کرد:


در اینجا ترتیب اجرای رخ‌دادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده می‌کنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت می‌دهد. برای نمونه، این مسیر خاص، id:2 را یافته‌است. از این id می‌توان برای دسترسی به مجموعه‌ای از رخ‌دادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شده‌است.
- یکی از روش‌هایی که برای دیباگ مسیریابی‌ها می‌توان استفاده کرد، همین فعالسازی enableTracing است.


کار با رخ‌دادهای مسیریابی با کدنویسی

به رخ‌دادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز می‌توان دسترسی یافت. برای مثال می‌توان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آن‌را مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آن‌ها باید مشترکشان شد:
this.router.events.subscribe((routerEvent: Event) => {
    if (routerEvent instanceof NavigationStart) {
      //...
    }
});
شیء router به همراه خاصیت events است که با گوش فرادادن به رخ‌دادهای صادر شده‌ی توسط آن می‌توان دریافت چه نوع رخ‌دادی هم اکنون صادر شده‌است.

در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربه‌ی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازه‌ی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا می‌خواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخ‌دادها را در نقطه‌ی آغازین برنامه و یا همان src\app\app.component.ts انجام می‌دهیم:
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';

import { AuthService } from './user/auth.service';
import { Component } from '@angular/core';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  pageTitle: string = 'Routing Lab';

  loading: boolean = true;

  constructor(private authService: AuthService,
    private router: Router) {
    router.events.subscribe((routerEvent: Event) => {
      this.checkRouterEvent(routerEvent);
    });
  }

  checkRouterEvent(routerEvent: Event): void {
    if (routerEvent instanceof NavigationStart) {
      this.loading = true;
    }

    if (routerEvent instanceof NavigationEnd ||
      routerEvent instanceof NavigationCancel ||
      routerEvent instanceof NavigationError) {
      this.loading = false;
    }
  }

  logOut(): void {
    this.authService.logout();
    this.router.navigateByUrl('/welcome');
  }
}
کدهای کامل AppComponent را جهت گوش فرادادن به رخ‌دادهای شروع و یا خاتمه/لغو/شکست پیمایش یک مسیریابی، در اینجا مشاهده می‌کنید.
- ابتدا وابستگی‌های لازم آن import شده‌اند.
- سپس می‌خواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آن‌را false خواهیم کرد.

اکنون برای استفاده‌ی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
با افزودن span فوق به ابتدای فایل app.component.html به تغییرات خاصیت loading واکنش نشان خواهیم داد. کلاس‌های CSS ایی را که در اینجا اضافه شده‌اند، به فایل src\styles.css اضافه می‌کنیم:
/* Spinner */
.spinner {
  font-size:300%;
  position:absolute;
  top: 50%;
  left: 50%;
  z-index:10
}

.glyphicon-spin {
    -webkit-animation: spin 1000ms infinite linear;
    animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}
@keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}


اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آن‌ها از سرور دریافت می‌شود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته می‌توان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
استفاده از GZip توکار IISهای جدید و تنظیمات مرتبط با آن‌ها
یکی از نقش‌های IISهای جدید (از نگارش 7 به بعد) که در ویندوز سرورهای قابل نصب است، نقش Performance است و ذیل آن دو نقش فشرده سازی استاتیک و پویا قابل انتخاب است. اگر این نقش‌ها بر روی سرور نصب باشند، دیگر نیازی به استفاده از HTTP Moduleهای متداول فشرده سازی صفحات وب نیست. برای استفاده‌ی از آن تنها کافی است کمی web.config را ویرایش کرد و ... گفته شده‌است که کار می‌کند! اما پس از اعمال تنظیمات، اگر به هدرهای خروجی Response صفحه در ابزارهای web developer مرورگرها دقت کنید، خبری از encoding جدیدی به نام gzip نیست (Content-Encoding: gzip) و به نظر اعمال نمی‌شود. در ادامه بررسی خواهیم کرد که چرا اینگونه است.


فعال سازی GZip توکار IIS

تنظیمات پیش فرض فعال سازی ماژول توکار GZip وب سرورهای جدید شامل دو مرحله است:
الف) تنظیمات سرور جهت فعال سازی فشرده سازی
بر روی ویندوزهای سرور، پس از مراجعه به Administrative Tools -> Server Manager و گشودن Roles آن، ذیل قسمت Web Server که در اینجا IIS است، نیاز است نقش جدیدی به نام Performance اضافه شود و مطابق تصویر، هر دو گزینه‌ی فشرده سازی استاتیک و پویا انتخاب گردد.


بنابراین اولین قدم برای عیب یابی کار نکردن GZip توکار IIS، از این مرحله شروع می‌شود که آیا اصلا ماژول مربوطه نصب هست یا خیر؟

ب) تنظیمات برنامه جهت فعال سازی ماژول GZip
پس از اطمینان از نصب ماژول توکار فشرده سازی صفحات وب IIS در سمت تنظیمات سرور، اکنون باید چند سطر ذیل را به Web.Config برنامه اضافه کرد:
  <system.webServer>

    <httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files">
      <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" staticCompressionLevel="9" />
      <dynamicTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/x-javascript" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="application/json" enabled="true" />
        <add mimeType="application/json; charset=utf-8" enabled="true" />
        <add mimeType="application/atom+xml" enabled="true" />
        <add mimeType="application/xaml+xml" enabled="true" />
        <add mimeType="*/*" enabled="false" />
      </dynamicTypes>
      <staticTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/x-javascript" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="application/json" enabled="true" />
        <add mimeType="application/json; charset=utf-8" enabled="true" />
        <add mimeType="application/atom+xml" enabled="true" />
        <add mimeType="application/xaml+xml" enabled="true" />
        <add mimeType="*/*" enabled="false" />
      </staticTypes>
    </httpCompression>
    <urlCompression doStaticCompression="true" doDynamicCompression="true" />

  </system.webServer>
در اینجا تنظیمات مخصوص نحوه‌ی فعال سازی فشرده سازی توکار صفحات پویا و فایل‌های استاتیک را مشاهده می‌کنید. در این تنظیمات محل قرارگیری فایل‌های موقتی فشرده شده‌ی توسط این ماژول و همچنین mime typeهای مدنظر جهت فشرده سازی، ذکر شده‌اند. با این تنظیمات، تنها mime typeهایی که به صورت صریح ذکر شده‌اند فشرده خواهند شد و از سایر mime types صرفنظر می‌شود.
این تنظیماتی است که در اکثر سایت‌ها نیز یافت می‌شود. ذکر آن‌ها اجباری است و پس از اعمال، اگر برنامه را اجرا کنید ... چیزی فشرده نمی‌شود! علت اصلی را باید در تنظیماتی یافت که مخصوص سرور است و در اینجا ذکر نشده‌اند.


تنظیمات مخصوص آستانه‌ی فشرده سازی صفحات

علت اصلی عدم مشاهده‌ی هدر gzip، در Response برنامه، به frequent hit threshold تنظیم شده‌ی در IIS بر می‌گردد. مقدار آن به 2 درخواست در طی 10 ثانیه تنظیم شده‌است. یعنی اگر به صفحه‌ای در طی 10 ثانیه دو درخواست نرسد، فشرده نخواهد شد. این تنظیم را می‌توان با مراجعه‌ی به configuration editor نود اصلی سرور وب در IIS manager، ویرایش کرد:



برای نمونه در تصویر فوق، این آستانه به یک درخواست در طی 10 ساعت تنظیم شده‌است. این عدد سبب خواهد شد تا تمامی درخواست‌های رسیده حتما فشرده سازی شوند.
این تنظیم معادل یک سطر ذیل در فایل web.config است. اما چون قسمت system.webServer/serverRuntime در تنظیمات سرور قفل شده‌است، هیچ تاثیری نخواهد داشت و حتما باید در سمت سرور و توسط IIS manager اعمال شود:
<system.webServer>
   <serverRuntime frequentHitThreshold="1" frequentHitTimePeriod="10:00:00" />
</system.webServer>
برای آزاد سازی این تنظیمات نیاز است دستور ذیل بر روی سرور اجرا شود. پس از آن کاربران برنامه‌های وب می‌توانند از تنظیمات وب کانفیگ خاص خود استفاده کنند:
 C:\Windows\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/serverRuntime

یک نکته
اگر از سرورهای پس از 2008 استفاده می‌کنید، گزینه‌ی staticCompressionIgnoreHitFrequency نیز به تنظیمات serverRuntime اضافه شده‌است که با تنظیم آن به true، از این حد آستانه، برای فایل‌های استاتیک صرفنظر خواهد شد.


تنظیمات مخصوص اندازه‌ی فایل‌هایی که باید فشرده سازی شوند

تنها حد آستانه‌ی درخواست صفحات وب نیست که بر روی فشرده سازی یا عدم آن ثاثیرگذار است. در اینجا میزان CPU Usage سیستم و یا حتی اندازه‌ی Response خروجی نیز مهم هستند که نمونه‌ای از تنظیمات آن‌را در شکل ذیل مشاهده می‌کنید:


در اینجا با تنظیم minFileSizeForComp به 1024، اعلام شده‌است که حجم‌هایی کمتر از یک کیلوبایت، فشرده سازی نشوند (مقدار پیش فرض آن، بیش از این عدد است).
البته این عدد را به شکل زیر نیز می‌توان به تنظیمات httpCompression وب کانفیگ اضافه کرد:
 <httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files" minFileSizeForComp="1024">

پس از اعمال این تنظیمات نیاز است یکبار IIS را نیز ری استارت کرد.


نتیجه گیری

اگر پس از فعال سازی GZip وب سرور، خروجی برنامه فشرده سازی نشد (Content-Encoding: gzip)، علت اینجا است که هنوز 2 درخواست مورد نیاز، در طی 10 ثانیه به سمت سرور ارسال نشده‌اند و تنظیمات پیش فرض این ماژول، جهت حداقل مصرف CPU و فشار بر روی سرور است.
مطالب
آشنایی با SharePoint Sandboxed Solutions
از Sandbox برای ساخت و انتشار وب پارت هایی استفاده می‌شود که نیاز به مداخله مدیریتی (administrative intervention) وجود ندارد. به این معنی که شما می‌توانید وب پارت خود را در محل شیرپوینت و هاست آن یا با استفاده از زیر ساخت‌های Cloud و دور از هاست آن ، بارگذاری کرده و انتشار دهید.
هر Solution که برای Sandbox ایجاد می‌شود ، در یک گالری خاص به نام Solution Gallery در Site Collection مربوطه ایجاد می‌شود و تمام این solution‌ها می‌توانند قعال یا غیر فعال شوند .


برای استفاده از این مزایا باید سرویس Microsoft SharePoint Foundation User Code Service فعال باشد .

 



استفاده از Sandbox Solution نیازمند 3 چیز است :

1 - User Code Service یا SPUCHostService.exe : این سرویس وظیفه مدیریت کدهای Sandbox روی سرور و میزبانی درخواست‌های Sandbox به سرور را دارد.

2 - Sandbox Worker Process یا SPUCWorkerProcess.exe : این سرویس برای مدیریت پردازش کدهای اجرایی Sandbox استفاده می‌شود خصوصا در مورارد امنیتی (فرض کنید درون کد حلقه بی نهایتی وجود داشته باشد. اگر بار اجرایی آن بر روی w3wp.exe باشد ، باعث متوقف شدن کارکرد تمام پروسه می‌شود. بار این مسئولیت بر دوش SPUCWorkerProcess.exe می‌باشد ) 

3 - Sandbox Worker Process Proxy یا SPUCWorkerProcessProxy.exe : این سرویس برای اجرای کدهای Client Object Model در sandbox استفاده می‌شود. 


می توانید ارتباط پردازه‌های فوق را در نمودار زیر مشاهد کنید :



با توجه به این توضیحات به دلیل استفاده از Sandbox می‌پردازیم :

1 - sandbox Solution‌ها امن هستند. 

2 - sandbox Solution‌ها می‌توانند مانیتور شوند. 

3 - sandbox Solution‌ها حداقل تاثیر را روی هم دارند. 

4 - sandbox Solution‌ها عملیات I/O کمی روی فایل‌های سیستمی دارند 

5 - sandbox Solution‌ها امکان debugging دارند 

6 - sandbox Solution‌ها امکان توسعه ، انتشار و به روز رسانی دارند 

7 - sandbox Solution‌ها امکان تعریف Policy برای CAS ایجاد می‌کند (Code Access Security برای قابل اطمینان ساخت کد ها) 

8 - sandbox Solution‌ها امکان اعتبار سنجی به کمک کلاس SPSolutionValidator را می‌دهد. 

9 - sandbox Solution‌ها امکان تعریف SLA های مختلف را می‌دهد. 



چه قسمت هایی توسط Sandbox Solution پشتیبانی می‌شود :

 

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

 اسفاده محدود از لیست‌ها ، کار با Web Template‌ها و ویژگی‌های آنها ، Content Type‌ها و فیلد‌ها ، ماژول‌ها و فایل‌ها ، وب پارت‌های مشتق شده از کلاس WebPart ، برخی Event Receiver‌ها ، بعضی از Custom Action و چرخه‌های کاری . و از مواردی که نمی‌توان در sandbox استفاده کرد می‌توان به ویژگی‌های موجود در Web Application ، ویژگی‌های موجود در Farm و Timer job ، استفاده از عملیات I/O و استفاده از ADO و برخی کلاس‌های دیگر در دات نت اشاره کرد .
مدیران سایت می‌توانند Solution‌ها را مونیتور کنند و برای Resource Point هایی که در یک روز مصرف می‌کنند Quota تعریف کنند .پیش فرض این مقدار 300 Resource Point در روز است .
 هر solution در هنگام deploy شدن جهت معتبر بودن چک می‌شود و سپس منتشر شود. در صورتی که Valid نباشد ، خطایی به برنامه نویس نشان می‌دهد.
 در انتها اشاره ای هم به استفاده از STSADM یا Powershell برای deploy کردن solution می‌کنم که توسط دستورات زیر قابل اجراست :
 
Add-SPSolution c:\code\SharePointProject2\bin\debug\SharePointProject2.wsp
stsadm –o addsolution –name SharePointProject2.wsp
همانطور که پسوند فایل‌های solution را مشاهده می‌کند wsp هستند که اگر آنها را به cab تغییر نام دهید میتوانید محتویات آنها را مشاهده کنید
 
نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
یک نکته‌ی تکمیلی: روش تعیین حداکثر اندازه‌ی فایل قابل آپلود در برنامه‌های ASP.NET Core
الف) اگر برنامه توسط IIS هاست شده‌است
<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <!-- To customize the asp.net core module uncomment and edit the following section. 
 For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->
 <system.webServer>
  <handlers>
  <remove name="aspNetCore"/>
  <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
  </handlers>
  <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
  <security>
   <requestFiltering>
    <!-- This will handle requests up to 50MB -->
    <requestLimits maxAllowedContentLength="52428800" />
   </requestFiltering>
  </security>
  </system.webServer>
</configuration>
در اینجا (در فایل web.config برنامه) مقدار خاصیت maxAllowedContentLength است که تعیین کننده‌ی حداکثر اندازه‌ی فایل قابل آپلود است. به علاوه مطمئن شوید که فایل web.config شما حتما publish شده.
همچنین requestTimeout را نیز به صورت زیر می‌توان مقدار دهی کرد:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <aspNetCore requestTimeout="00:20:00"  .... />
  </system.webServer>
</configuration>
به همراه این تنظیمات:
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<IISServerOptions>(options =>
            {
                options.MaxRequestBodySize = int.MaxValue;
            });
            services.Configure<FormOptions>(options =>
            {
                options.ValueLengthLimit = int.MaxValue;
                options.MultipartBodyLengthLimit = long.MaxValue; // <-- ! long.MaxValue
                options.MultipartBoundaryLengthLimit = int.MaxValue;
                options.MultipartHeadersCountLimit = int.MaxValue;
                options.MultipartHeadersLengthLimit = int.MaxValue;
            });
 
ب) اگر برنامه در لینوکس و بر اساس وب سرور Kestrel هاست شده‌است
روش اول: استفاده از ویژگی RequestSizeLimit که از نگارش ASP.NET Core 2.0 به بعد قابل استفاده‌است و صرفا به قسمتی از برنامه اعمال می‌شود:
[HttpPost]
[RequestSizeLimit(40000000)]
public async Task<IActionResult> UploadFiles(IFormFile file)
روش دوم: تنظیم سراسری آن برای کل برنامه:
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        .UseStartup<Startup>()
                        .ConfigureKestrel(kestrelServerOptions =>
                        {
                            kestrelServerOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10);
                            kestrelServerOptions.Limits.MaxRequestBodySize = 52428800; //50MB
                        });
                });
بدیهی است تنظیم این دو روش در حالت استفاده‌ی از IIS تاثیری نخواهند داشت.
مطالب
اعمال کنترل دسترسی پویا در پروژه‌های ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی

در مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا» به طور مفصل به قضیه کنترل دسترسی پویا در ASP.NET Core Identity پرداخته شده‌است؛ در این مطلب روش دیگری را بررسی خواهیم کرد.

مشخص می‌باشد که بدون وابستگی به روش خاصی، خیلی ساده می‌توان به شکل زیر عمل کرد:

services.AddAuthorization(options =>
{
    options.AddPolicy("View Projects", 
        policy => policy.RequireClaim(CustomClaimTypes.Permission, "projects.view"));
});
با یک ClaimType مشخص برای دسترسی‌ها، یک سیاست جدید را تعریف کرده و برای استفاده از آن نیز همانند قبل به شکل زیر می‌توان عمل کرد:
[Authorize("View Projects")]
public IActionResult Index(int siteId)
{
    return View();
}
روشی یکپارچه و بدون نیاز به کوچکترین سفارشی سازی؛ ولی در مقیاس بزرگ تعریف سیاست‌ها برای تک تک دسترسی‌های مورد نیاز، قطعا آزار دهنده خواهد بود. خبر خوب اینکه زیرساخت احراز هویت و کنترل دسترسی در ASP.NET Core مکانیزمی برای خودکار کردن فرآیند تعریف options.AddPolicy‌ها در کلاس آغازین برنامه، ارائه داده‌است که دقیقا یکی از موارد استفاده‌ی آن، راه حلی می‌باشد برای همین مشکلی که مطرح شد.
Using a large range of policies (for different room numbers or ages, for example), so it doesn’t make sense to add each individual authorization policy with an AuthorizationOptions.AddPolicy call. 


کار با پیاده سازی واسط IAuthorizationPolicyProvider شروع می‌شود؛ یا شاید ارث بری از DefaultAuthorizationPolicyProvider رجیستر شده‌ی در سیستم DI و توسعه آن هم کافی باشد.

public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
        : base(options)
    {
    }

    public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        if (!policyName.StartsWith(PermissionAuthorizeAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase))
        {
            return base.GetPolicyAsync(policyName);
        }

        var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(',');

        var policy = new AuthorizationPolicyBuilder()
            .RequireClaim(CustomClaimTypes.Permission, permissionNames)
            .Build();

        return Task.FromResult(policy);
    }
}

متد GetPolicyAsync موظف به یافتن و بازگشت یک Policy ثبت شده می‌باشد؛ با این حال می‌توان با بازنویسی آن و با استفاده از وهله‌ای از AuthorizationPolicyBuilder، فرآیند تعریف سیاست درخواست شده را که احتمالا در تنظیمات آغازین پروژه تعریف نشده و پیشوند مدنظر را نیز دارد، خوکار کرد. در اینجا امکان ترکیب کردن چندین دسترسی را هم خواهیم داشت که برای این منظور می‌توان دسترسی‌های مختلف را به صورت comma separated به سیستم معرفی کرد. 

نکته‌ی مهم در تکه کد بالا مربوط است به PolicyPrefix که با استفاده از آن مشخص کرده‌ایم که برای هر سیاست درخواستی، این فرآیند را طی نکند و موجب اختلال در سیستم نشود. 


پس از پیاده سازی واسط مطرح شده، لازم است این پیاده سازی جدید را به سیستم DI هم معرفی کنید:

services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();


خوب، تا اینجا فرآیند تعریف سیاست‌ها به صورت خودکار انجام شد. در ادامه نیاز است با تعریف یک فیلتر Authorization، بتوان لیست دسترسی‌های مورد نظر برای اکشنی خاص را نیز مشخص کرد تا در متد GetPolicyAsync فوق، کار ثبت خودکار سیاست دسترسی متناظر با آن‌را توسط فراخوانی متد policyBuilder.RequireClaim، انجام دهد تا دیگر نیازی به تعریف دستی و جداگانه‌ی آن، در کلاس آغازین برنامه نباشد. برای این منظور به شکل زیر عمل خواهیم کرد:

    public class PermissionAuthorizeAttribute : AuthorizeAttribute
    {
        internal const string PolicyPrefix = "PERMISSION:";

        /// <summary>
        /// Creates a new instance of <see cref="AuthorizeAttribute"/> class.
        /// </summary>
        /// <param name="permissions">A list of permissions to authorize</param>
        public PermissionAuthorizeAttribute(params string[] permissions)
        {
            Policy = $"{PolicyPrefix}{string.Join(",", permissions)}";
        }
    }

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

[PermissionAuthorize(PermissionNames.Projects_View)]
public IActionResult Get(FilteredQueryModel query)
{
   //...
}
[PermissionAuthorize(PermissionNames.Projects_Create)]
public IActionResult Post(ProjectModel model)
{
   //...
}

برای مثال در اولین فراخوانی فیلتر PermissionAuthorize فوق، مقدار ثابت PermissionNames.Projects_View به عنوان یک Policy جدید به متد GetPolicyAsync کلاس AuthorizationPolicyProvider سفارشی ما ارسال می‌شود. چون دارای پیشوند «:PERMISSION» است، مورد پردازش قرار گرفته و توسط متد policyBuilder.RequireClaim به صورت خودکار به سیستم معرفی و ثبت خواهد شد.


همچنین راه حل مطرح شده برای مدیریت دسترسی‌های پویا، در gist به اشتراک گذاشته شده «موجودیت‌های مرتبط با مدیریت دسترسی‌های پویا» را نیز مد نظر قرار دهید.

بازخوردهای پروژه‌ها
مشکل عمل نکردن فونت فارسی
سلام
باتشکر از شما
آقای نصیری بنده برای راحتی استفاده در برنامه یک کلاس استاتیک بصورت زیر تعریف کرده ام :
  public static class ReportMethod
    {
        static FontSelector fontSelector = new FontSelector();
        const char RightToLeftEmbedding = (char)PersianDate.RightToLeftEmbedding;
        const char PopDirectionalFormatting = (char)PersianDate.PopDirectionalFormatting;
        public static Dictionary<string, string> fontDicBody = new Dictionary<string, string> { { "BMitra", "B Mitra" }, { "tahoma", "tahoma" } };
        public static Dictionary<string, string> fontDicHeader1 = new Dictionary<string, string> { { "BMitraBd", "B Mitra Bold" }, { "tahoma", "tahoma" } };
        public static Dictionary<string, string> fontDicHeader2 = new Dictionary<string, string> { { "BTitrBd", "B Titr Bold" }, { "tahoma", "tahoma" } };
        public static Dictionary<string, string> fontDicFooter = new Dictionary<string, string> { { "BMitra", "B Mitra" }, { "tahoma", "tahoma" } };

        public static string FixWeakCharacters(string data)
        {
            if (string.IsNullOrWhiteSpace(data))
                return string.Empty;
            var weakCharacters = new[] { @"\", "/", "+", "-", "=", ";", "$" };
            foreach (var weakCharacter in weakCharacters)
            {
                data = data.Replace(weakCharacter, RightToLeftEmbedding + weakCharacter + PopDirectionalFormatting);
            }
            return data;
        }

        public static Phrase SetFont(string data, int fontType)
        {
            Dictionary<string, string> fontDic;
            float fontSize = 11;
            switch (fontType)
            {
                case 0:
                    fontDic = fontDicBody;
                    fontSize = 11;
                    break;
                case 1:
                    fontDic = fontDicHeader1;
                    fontSize = 14;
                    break;
                case 2:
                    fontDic = fontDicFooter;
                    fontSize = 12;
                    break;
                case 11:
                    fontDic = fontDicHeader2;
                    fontSize = 18;
                    break;
                default:
                    fontDic = fontDicBody;
                    fontSize = 11;
                    break;
            }
            foreach (var item in fontDic)
            {
                FontFactory.Register("c:\\windows\\fonts\\" + item.Key + ".ttf");
                Font newfont = FontFactory.GetFont(item.Value, BaseFont.IDENTITY_H, fontSize);
                if (newfont.Familyname != "unknown")
                    fontSelector.AddFont(newfont);
            }
            return fontSelector.Process(FixWeakCharacters(data));
        }

        public static PdfPCell SetCell(string text, int border, int colspan, int Horizontal, int Vertical, bool DirectionRTL, int fontType = 0)
        {
            if (DirectionRTL)
            {
                var cell = new PdfPCell { RunDirection = PdfWriter.RUN_DIRECTION_RTL };
                cell.Border = border;
                cell.Colspan = colspan;
                cell.HorizontalAlignment = Horizontal;
                cell.VerticalAlignment = Vertical;
                cell.Phrase = new Phrase(ReportMethod.SetFont(text, fontType));
                return cell;
            }
            else
            {
                var cell = new PdfPCell();
                cell.Border = border;
                cell.Colspan = colspan;
                cell.HorizontalAlignment = Horizontal;
                cell.VerticalAlignment = Vertical;
                cell.Phrase = new Phrase(ReportMethod.SetFont(text, fontType));
                return cell;
            }
        }
    }
که این کلاس برای ایجاد سلول با فونت مورد نظر من معرفی شده است
و کد گزارش من به صورت زیر تعریف شده است :
    public IPdfReportData CreatePdfReport_SRptTeach(int MemberID, List<sp_Teach_Communicate_Select_ReportTeachResult> Teach_Result, string st, List<sp_Institute_Center_Info_Select_Name_MasterResult> Info)
        {
            string fileName = string.Format("SRptTeach-{0}.pdf", Guid.NewGuid().ToString("N"));            
            return new PdfReport()
                                  .DocumentPreferences(doc =>
                                  {
                                      doc.RunDirection(PdfRunDirection.RightToLeft);
                                      doc.Orientation(PageOrientation.Landscape);
                                      doc.PageSize(PdfPageSize.A4);
                                      doc.DocumentMetadata(new DocumentMetadata { Author = Info[0].InstName, Application = "PdfRpt", Keywords = "گزارش", Subject = "گزارش ویژه", Title = "گزارش کارکرد مدرسید" });
                                      doc.Compression(new CompressionSettings
                                      {
                                          EnableCompression = true,
                                          EnableFullCompression = true
                                      });
                                      doc.PrintingPreferences(new PrintingPreferences
                                      {
                                          ShowPrintDialogAutomatically = false
                                      });
                                  })
                                  .DefaultFonts(fonts =>
                                  {
                                      fonts.Path(System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "fonts\\" + ReportMethod.fontDicBody.ElementAt(0).Key + ".ttf"),
                                          System.IO.Path.Combine(Environment.GetEnvironmentVariable("SystemRoot"), "fonts\\" + ReportMethod.fontDicBody.ElementAt(1).Key + ".ttf"));
                                      fonts.Size(11);
                                      fonts.Color(System.Drawing.Color.Black);
                                  })
                                  .PagesFooter(footer =>
                                  {
                                      footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.RightToLeft));
                                  })
                                  .PagesHeader(header =>
                                  {
                                      header.CustomHeader(new CustomHeader_SRptTeach(MemberID, st, Info));
                                  })
                                  .MainTableTemplate(template =>
                                  {
                                      //template.BasicTemplate(BasicTemplate.SimpleTemplate);
                                      template.CustomTemplate(new TransparentTemplate());
                                  })
                                  .MainTablePreferences(table =>
                                  {
                                      table.ColumnsWidthsType(TableColumnWidthType.Relative);
                                      table.GroupsPreferences(new GroupsPreferences
                                      {
                                          GroupType = GroupType.IncludeGroupingColumns,
                                          RepeatHeaderRowPerGroup = true,
                                          ShowOneGroupPerPage = false,
                                          SpacingBeforeAllGroupsSummary = 5f,
                                          ShowGroupingPropertiesInAllRows = true
                                      });
                                  })
                                  .MainTableDataSource(dataSource =>
                                  {
                                      dataSource.StronglyTypedList<sp_Teach_Communicate_Select_ReportTeachResult>(Teach_Result);
                                  })
                                  .MainTableSummarySettings(summarySettings =>
                                  {
                                      summarySettings.OverallSummarySettings("جمع مبالغ");
                                      summarySettings.AllGroupsSummarySettings("جمع کل مبالغ");
                                  })
                                  .MainTableColumns(columns =>
                                  {
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.RowNo);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(0);
                                          column.Width(4);
                                          column.HeaderCell("ردیف");
                                      });

                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.ParentName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(1);
                                          column.Width(5);
                                          column.HeaderCell("مرکز");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.FullName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(2);
                                          column.Width(12);
                                          column.HeaderCell("نام و نام خانوادگی");
                                          column.Group(true,
                                              (val1, val2) =>
                                              {
                                                  return val1.ToString() == val2.ToString();
                                              });
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TermInfoName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(3);
                                          column.Width(8);
                                          column.HeaderCell("ترم");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Contract_NO);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(4);
                                          column.Width(7);
                                          column.HeaderCell("شماره قرارداد");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.LessonFullCode);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(5);
                                          column.Width(4);
                                          column.HeaderCell("کد درس");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.LessonName);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(6);
                                          column.Width(10);
                                          column.HeaderCell("نام درس");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Start_Date_Lesson);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(7);
                                          column.Width(6);
                                          column.HeaderCell("تاریخ شروع");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.End_Date_Lesson);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(8);
                                          column.Width(6);
                                          column.HeaderCell("تاریخ پایان");
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TeachAmount);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(9);
                                          column.Width(6);
                                          column.HeaderCell("مبلغ حق التدریس(ریال)");
                                          column.ColumnItemsTemplate(template =>
                                          {
                                              template.TextBlock();
                                              template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                                          });
                                      });
                                      columns.AddColumn(column =>
                                      {
                                          column.PropertyName<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.DoTeacherTime);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(10);
                                          column.Width(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("CanPay");
                                          column.CalculatedField(true,
                                              list =>
                                              {
                                                  if (list == null)
                                                      return string.Empty;
                                                  var amount = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.TeachAmount);
                                                  var doTime = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.DoTeacherTime);
                                                  var result = float.Parse(amount) * float.Parse(doTime);
                                                  return Convert.ToDecimal(result);
                                              });
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(11);
                                          column.Width(7);
                                          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<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Personal_Education);
                                          column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                          column.IsVisible(true);
                                          column.Order(12);
                                          column.Width(6);
                                          column.HeaderCell("مدرک تحصیلی");
                                      });
                                      //columns.AddColumn(column =>
                                      //{
                                      //    column.PropertyName("Descriptions");
                                      //    column.CalculatedField(true,
                                      //        list =>
                                      //        {
                                      //            if (list == null)
                                      //                return string.Empty;
                                      //            var Row = list.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.RowNoPerson);
                                      //            return "";
                                      //        });
                                      //    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                                      //    column.IsVisible(true);
                                      //    column.Order(13);
                                      //    column.Width(3);
                                      //    column.HeaderCell("توضیحات");
                                      //});
                                  })
                                  .MainTableEvents(events =>
                                  {
                                      events.DataSourceIsEmpty(message: "اطلاعاتی برای نمایش وجود ندارد.");
                                      events.DocumentClosing(docClose =>
                                      {
                                          string[] msgField = { "مدیر گروه", Info.Where(sp => sp.ID == MemberID).FirstOrDefault().InstKindName, Info.Where(sp => sp.ID == 0).FirstOrDefault().InstKindName, "امور مالی", "معاون پشتیبانی" };
                                          string[] dataField = { "", Info.Where(sp => sp.ID == MemberID).FirstOrDefault().MasterName, Info.Where(sp => sp.ID == 0).FirstOrDefault().MasterName, "", Info.Where(sp => sp.ID == 1).FirstOrDefault().MasterName };
                                          var infoTable = new PdfGrid(msgField.Length) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, WidthPercentage = 100 };
                                          foreach (var item in msgField)
                                          {
                                              infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true));
                                          }
                                          foreach (var item in dataField)
                                          {
                                              infoTable.AddCell(ReportMethod.SetCell(item, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true));
                                          }
                                          docClose.PdfDoc.Add(infoTable);
                                      });
                                  })
                                  .Export(export =>
                                  {
                                      export.ToExcel();
                                      export.ToCsv();
                                      export.ToXml();
                                      export.ToString();
                                  })
                                  .Generate(data =>
                                  {
                                      fileName = HttpUtility.UrlEncode(fileName, Encoding.UTF8);
                                      data.FlushInBrowser(fileName, FlushType.Inline);
                                  });
            //.Generate(data => data.AsPdfFile(string.Format("{0}\\PlansPage\\RptIListSample-{1}.pdf", AppPath.ApplicationPath, Guid.NewGuid().ToString("N"))));
        }
و قسمت هدر گزارش به صورت سفارشی به صورت زیر معرفی شده است :
namespace Academy.Control.Reports
{
    public class CustomHeader_SRptTeach : IPageHeader
    {
        public IPdfFont PdfRptFont { set; get; }
        string st;
        List<sp_Institute_Center_Info_Select_Name_MasterResult> Info;
        int MemberID;

        public CustomHeader_SRptTeach(int MemberID, string st, List<sp_Institute_Center_Info_Select_Name_MasterResult> Info)
        {
            this.st = st;
            this.Info = Info;
            this.MemberID = MemberID;
        }

        public PdfGrid RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> rowdata, IList<SummaryCellData> summaryData)
        {
            // return null;
            var groupFullName = rowdata.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.FullName);
            var groupPersonalEducation = rowdata.GetSafeStringValueOf<sp_Teach_Communicate_Select_ReportTeachResult>(x => x.Personal_Education);

            var table = new PdfGrid(2) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "نام و نام خانوادگی:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = groupFullName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "مدرک تحصیلی :";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = groupPersonalEducation;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 10f);

        }    

        public PdfGrid RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            var tableMain = new PdfGrid(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, WidthPercentage = 100 };
            tableMain.DefaultCell.Border = PdfPCell.NO_BORDER;
            PdfGrid table = new PdfGrid(3);
            table.DefaultCell.Border = PdfPCell.NO_BORDER;

            table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false));
            table.AddCell(ReportMethod.SetCell("گزارش کارکرد مدرسین ", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, true,11));
            PdfPTable tbRight = new PdfPTable(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL };
            tbRight.DefaultCell.Border = PdfPCell.NO_BORDER;

            Image _image = Image.GetInstance(System.IO.Path.Combine(AppPath.ApplicationPath, "Content\\Images\\p_jahad2.jpg"));
            var cellImg = new PdfPCell(_image, false) { Border = PdfPCell.NO_BORDER };
            cellImg.HorizontalAlignment = PdfPCell.ALIGN_CENTER;
            tbRight.AddCell(cellImg);
            tbRight.AddCell(ReportMethod.SetCell(Info[0].InstName, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false,1));
            tbRight.AddCell(ReportMethod.SetCell(Info.Where(sp => sp.ID == MemberID).FirstOrDefault().SecondName, PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false,1));
            table.AddCell(tbRight);

            PdfGrid tbLeft = new PdfGrid(2) { RunDirection = PdfWriter.RUN_DIRECTION_RTL };
            tbLeft.DefaultCell.Border = PdfPCell.NO_BORDER;
            tbLeft.AddCell(ReportMethod.SetCell("تاریخ گزارش : " + System.DateTime.Now.ToPersianDateTime("/", false), PdfPCell.NO_BORDER, 2, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false));
            tbLeft.AddCell(ReportMethod.SetCell("از تاریخ " + st.Split(';')[0], PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false));
            tbLeft.AddCell(ReportMethod.SetCell("تا تاریخ " + st.Split(';')[1], PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_LEFT, PdfPCell.ALIGN_MIDDLE, false));

            table.AddCell(tbLeft);
            table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false));
            table.AddCell(ReportMethod.SetCell("", PdfPCell.NO_BORDER, 1, PdfPCell.ALIGN_CENTER, PdfPCell.ALIGN_MIDDLE, false));
            tableMain.AddCell(table);
            return tableMain;
        }
    }
}
حالا من دو مشکل دارم که هرچی سعی کردم نتونستم این موارد را رفع کنم و درخواست راهنمایی دارم از شما :
1. فونت هایی که من معرفی کردم برای هدر اصلا اعمال نمی‌شود و همینطور فونت در متن اصلی هم که تغییر میدم باز تغییر ایجاد نمیشه و به نظر میاد اصلا اعمال نمیشه به کل و هدر و متن با یک فونت نمایش داده میشه در صورتی که من فونت‌ها و سایز‌های متفاوتی برای متن‌ها انتخاب میکنم و اعمال میکنم
2. قسمت گروه هدری که من معرفی کردم اصلا کار نمیکنه و نمایش داده نمیشه
ممنون میشم شما من رو راهنمایی کنید 
مثال‌های قبلی رو هم دیدم در مورد فونت و گروه هدر و سعی کردم مثل همون موارد اعمال کنم اما باز اعمال نشد
متشکرم از وقتی که می‌گذارید