مطالب
روش نادرست اعمال دسترسی کاربر به منوها و کنترلهای فرم در windows form ها
معمولا وقتی صحبت از هک می‌شود بیشتر وب سایتها مدنظر هستند و کمتر به نرم افزارهای محیط desktop توجه می‌شود اما در محیط desktop هم امنیت بسیار مهم است.

یکی از مواردی که در رابطه با windows form‌ها (دات نتی و غیر دات نتی) بسیار به آن برخورد کرده ام اعمال دسترسی کاربر به قسمتهای مختلف فرم با مخفی یا غیر فعال کردن دکمه‌ها و کنترلهای فرم است. به این صورت که در هنگام لود فرم اصلی با توجه به دسترسی کاربر بعضی منوها مخفی می‌شوند و در لود هر فرم هم بعضی از دکمه‌ها و کنترلهای روی فرم مخفی یا غیر فعال می‌شوند. همین!

اما اگر کاربر به کمک ابزاری بتواند منوها و کنترلهای مخفی فرم را نمایش دهد و
منوها و کنترلهای غیرفعال را فعال کند چی؟

کلاس زیر را در نظر بگیرید :
    public class HackWindowsForms
    {
        struct POINTAPI
        {
            public int X;
            public int Y;
        }

        [DllImport("user32.dll")]
        static extern int EnumChildWindows(int hWndParent, EnumChildCallbackDelegate enumChildCallback, int lParam);
        [DllImport("user32.dll")]
        static extern int EnableWindow(int hwnd, int fEnable);
        [DllImport("user32.dll")]
        static extern int WindowFromPoint(int x, int y);
        [DllImport("user32.dll")]
        static unsafe extern int GetCursorPos(POINTAPI* lpPoint);

        delegate int EnumChildCallbackDelegate(int hwnd, int lParam);

        public static void EnableThisWindowControls()
        {
            unsafe
            {
                POINTAPI cursorPosition = new POINTAPI();
                GetCursorPos(&cursorPosition);
                int winWnd = WindowFromPoint(cursorPosition.X, cursorPosition.Y);
                EnumChildWindows(winWnd, EnumChildCallback, 0);
            }
        }


        static int EnumChildCallback(int hwnd, int lParam)
        {
            EnableWindow(hwnd, 1);
            EnumChildWindows(hwnd, EnumChildCallback, 0);
            return 1;
        }
    }
یک پروژه ویندوز فرم ایجاد کنید و دکمه ای روی فرم قرار دهید. برای MouseUp آن کد زیر را بنویسید :
        private void button1_MouseUp(object sender, MouseEventArgs e)
        {
            HackWindowsForms.EnableThisWindowControls();
            MessageBox.Show("حالا روی دکمه مورد نظر کلیک کنید و کلید space را فشار دهید.", "", MessageBoxButtons.OK, MessageBoxIcon.Asterisk, MessageBoxDefaultButton.Button1, MessageBoxOptions.RtlReading);
        }
حالا اگر برنامه را اجرا کنید و روی دکمه کلیک کنید و ماوس را نگهدارید سپس روی نوار عنوان پنجره مورد نظر رها کنید، تمام کنترلهای روی آن پنجره فعال خواهند شد!
نمونه کد مشابهی هم برای نمایش منوها و کنترلهای مخفی شده می‌توان نوشت.

اما راه حل چیست؟
قبل از اجرای هر عملی که احتیاج به دسترسی دارد، دسترسی کاربر چک شود و فقط به غیر فعال و مخفی کردن کنترلها بسنده نشود. مثلا اگر دکمه ویرایش را غیر فعال کرده ایم در کلیک آن هم دسترسی چک شود.
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت دوم

در قسمت اول با مفاهیم اولیه Class و Object آشنا شدیم.

Messages and Methods

Objectها باید مانند ماشین‌هایی تلقی شوند که عملیات موجود در واسط عمومی خود را برای افرادی که درخواست مناسبی ارسال کنند، اجرا خواهند کرد. با توجه به اینکه یک object از استفاده کننده خود مستقل است و وابستگی به او ندارد و همچنین توجه به ساختار نحوی (syntax) برخی از زبان‌های شیء گرای جدید، عبارت «sending a message» برای توصیف اجرای رفتاری از مجموعه رفتارهای object، استفاده میشود.
به محض اینکه پیغامی (Message) به سمت object ارسال شود، ابتدا باید تصمیم بگیرد که این پیغام ارسالی را درک می‌کند. فرض کنیم این پیغام قابل درک است. در این صورت object مورد نظر، همزمان با نگاشت پیغام به یک فراخوانی تابع (function call)، خود را به صورت ضمنی به عنوان اولین آرگومان ارسال می‌کند. تصمیم گرفتن در رابطه با قابل درک بودن یک پیغام، در زبان‌های مفسری در زمان اجرا و در زبان‌های کامپایلری در زمان کامپایل، انجام میگرد. 
نام (یا prototype) رفتار یک وهله، Message (پیغام) نامیده می‌شود. بسیاری از زبان‌های شیء گرا مفهموم Overloaded Functions Or Operators را پشتیبانی می‌کنند. در این صورت می‌توان در سیستم دو تابعی داشت که با نام یکسان، یا انواع مختلف آرگومان (intraclass overloading) داشته باشند یا در کلاس‌های مختلفی (interclass overloading) قرار گیرند. 
ممکن است کلاس ساعت زنگدار، دو پیغام set_time که یکی از آنها با دو آرگومان از نوع عدد صحیح و دیگری یک آرگومان رشته‌ای است داشته باشد.

void AlarmClock::set_time(int hours, int minutes); 

void AlarmClock::set_time(String time);

در مقابل، کلاس ساعت زنگدار و کلاس ساعت مچی هر دو messageای به نام set_time با دو آرگومان از نوع عدد صحیح دارند.

void AlarmClock::set_time(int hours, int minutes); 

void Watch::set_time(int hours, int minutes);

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

در برخی از زبان‌ها و یا سیستم‌ها، اطلاعات دیگری مانند: انواع استثناءهایی که از سمت پیغام پرتاب می‌شوند تا اطلاعات همزمانی (پیغام به صورت همزمان است یا ناهمزمان) را برای استفاده کننده مهیا کنند. از طرفی پیاده سازی کنندگان یک کلاس باید از پیاده سازی پیغام آگاه باشند. جزئیات پیاده سازی یک پیغام -کدی که پیغام را پیاده سازی می‌کند- Method (متد) نامیده میشود. آنگاه که نخ (thread) کنترل درون متد باشد، برای مشخص کردن اینکه پیغام رسیده برای کدام وهله ارسال شده‌است، ارجاعی به وهله مورد نظر و به عنوان اولین آرگومان، به صورت ضمنی ارسال می‌شود. این آرگومان ضمنی، در بیشتر زبان‌ها Self Object نامیده می‌شود (در سی پلاس پلاس this object نام دارد). در نهایت، مجموعه پیغام‌هایی که یک وهله می‌تواند به آنها پاسخ دهد، Protocol (قرارداد) نام دارد.

دو پیغام خاصی که کلاس‌ها یا وهله‌ها می‌توانند به آنها پاسخ دهند، اولی که استفاده کنندگان کلاس برای ساخت وهله‌ها از آن استفاده می‌کنند، Constructor (سازنده) نام دارد. هر کلاسی می‌تواند سازنده‌های متعددی داشته باشد که هر کدام مجموعه پارامترهای مختلفی را برای مقدار دهی اولیه می‌پذیرند. دومین پیغام، عملیاتی است که وهله را قبل از حذف از سیستم، پاک سازی می‌کند. این عملیات، Destructor (تخریب کننده) نام دارد. بیشتر زبان‌های شیء گرا، برای هر کلاس تنها یک تخریب کننده دارند. این پیغام‌ها را به عنوان مکانیزم مقدار دهی اولیه و پاک سازی در پارادایم شیء گرا در نظر بگیرید.

قاعده شهودی 2.2

استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)

منطق پشت این قاعده، یکی از شکل‌های قابلیت استفاده مجدد (resuability) می‌باشد. یک ساعت زنگدار ممکن است توسط شخصی در اتاق خواب او استفاده شود. واضح است که شخص مورد نظر به واسط عمومی ساعت زنگدار وابسته می‌باشد. به هر حال، ساعت زنگدار نباید به شخصی وابسته باشد. اگر ساعت زنگدار به شخصی وابسته باشد، بدون مهیا کردن یک شخص، نمی‌توان از آن برای ساخت یک TimeLockSafe استفاده کرد. این وابستگی‌ها برای مواقعیکه می‌خواهیم امکان این را داشته باشیم تا کلاس ساعت زنگدار را از دامین (domain) خود خارج کرده و در دامین دیگری، بدون وابستگی هایش مورد استفاده قرار دهیم، نامطلوب هستند.

شکل 2.4 The Use Of Alarm Clocks

 The Use Of Alarm Clocks قاعده شهودی 2.3

تعداد پیغام‌های موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class)

چندین سال قبل، مطلبی منتشر شد که کاملا متضاد این قاعده شهودی می‌باشد. طبق آن، پیاده سازی کننده یک کلاس می‌تواند یکسری عملیات را با فرض اینکه در آینده مورد استفاده قرار گیرند، برای آن در نظر بگیرد. ایراد این کار چیست؟ اگر شما از این قاعده پیروی کنید، حتما کلاس LinkedList من، توجه شما را جلب خواهد کرد؛ این کلاس در واسط عمومی خود 4000 عملیات را دارد. فرض کنید قصد ادغام دو وهله از این کلاس را داشته باشید. در این صورت حتما فرض شما این است که عملیاتی تحت عنوان merge در این کلاس تعبیه شده است. بعد از جستجوی بین این تعداد عملیات، در نهایت این عملیات خاص را پیدا نخواهید کرد. چراکه این عملیات متأسفانه به صورت یک overloaded plus operator پیاده سازی شده است. مشکل اصلی واسط عمومی با تعداد زیادی عملیات این است که فرآیند یافتن عملیات مورد نظرمان را خیلی سخت یا حتی ناممکن خواهد کرد و مشکلی جدی برای قابلیت استفاده مجدد تلقی می‌شود.

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

قاعده شهودی 2.4

پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاس‌ها  (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].)

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

به عنوان مثال کلاس Object در دات نت به عنوان کلاس پایه ضمنی با یکسری از متدهای عمومی (برای مثال ToString)، نشان دهنده تعریف یک واسط عمومی مشترک برای همه کلاس‌ها در این فریمورک، می‌باشد.

public class Object
    {
        public Object();
        public static bool Equals(Object objA, Object objB){...}
        public static bool ReferenceEquals(Object objA, Object objB){...}
        public virtual bool Equals(Object obj){...}
        public virtual int GetHashCode(){...}
        public Type GetType(){...}
        public virtual string ToString(){...}
        protected Object MemberwiseClone(){...}
    }


قاعده شهودی 2.5 

جزئیات پیاده سازی، مانند توابع خصوصی common-code  ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید.  (Do not put implementation details such as common-code private functions into the public interface of a class)

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

شکل 2.5  Example of a common-code private function

Example of a common-code private function

مثال واقعی

فرض کنید در شکل بالا، کلاس X معادل یک LinkedList کلاس، f1و f2 به عنوان توابع insert و remove و تابع f به عنوان تابع common-code که عملیات یافتن آدرس را برای درج و حذف انجام می‌دهد، می‌باشند.

قاعده شهودی 2.6

واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید.  (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )

 این قاعده شهودی با قاعده قبلی که با قرار دادن تابع common-code در واسط عمومی کلاس، فقط باعث در هم ریختن واسط عمومی شده بود، مرتبط می‌باشد. در برخی از زبان‌ها مانند C++‎، برای مثال این امکان وجود دارد که سازنده یک کلاس انتزاعی (abstract) را در واسط عمومی آن قرار دهید؛ حتی با وجود اینکه در زمان استفاده از آن سازنده با خطای نحوی روبرو خواهید شد. این قاعده شهودی کلی، برای کاهش این مشکلات در نظر گرفته شده است. 

نظرات مطالب
نوع‌های نال نپذیر در TypeScript
یک نکته‌ی تکمیلی: اضافه شدن strictPropertyInitialization به TypeScript 2.7

در نگارش 2.7 اگر یک چنین تعریفی را داشته باشید:
export class MovieComponent {

  @Input() movie: Movie;

}
خطای زیر را دریافت خواهید کرد:
Error! Property movie has no initializer and is not assigned directly in the constructor.
یک روش برطرف کردن این خطا، نال‌پذیر تعریف کردن این خاصیت و سپس مقدار دهی اولیه‌ی به آن است:
@Input() movie: Movie | null = null;
و یا در مثال زیر می‌توان مشخص کرد که baz می‌تواند undefined هم باشد:
class C {
    baz: boolean | undefined;
}
روش دیگر، مقدار دهی اولیه، حین تعریف یک خاصیت است:
class C {
    bar = "hello";
}
و یا می‌توان این بررسی را به صورت زیر خاموش کرد:
class C {
    foo!: number;

    ngOnInit() {
        this.foo = 0;
    }
}
با استفاده از عملگر ! به کامپایلر اعلام می‌کنیم که این خاصیت حتما قرار است توسط روشی کمکی مقدار دهی اولیه شود و نال یا بدون مقدار نیست و استفاده‌ی از آن در این کلاس امن است.
مطالب
اشیاء Enumerable و Enumerator و استفاده از قابلیت‌های yield (قسمت اول)

در این مقاله می‌خواهیم نحوهٔ ساخت اشیایی با خصوصیات Enumerable را بررسی کنیم. بررسی ویژگی این اشیاء دارای اهمیت است حداقل به این دلیل که پایهٔ یکی از قابلیت مهم زبانی سی‌شارپ یعنی LINQ هستند. برای یافتن پیش‌زمینه‌ای در این موضوع خواندن این مقاله‌های بسیار خوب (۱ و  ۲) نیز توصیه می‌شود. 

Enumerableها

اشیاء Enumerable یا به‌عبارت دیگر اشیائی که اینترفیس IEnumerable را پیاده‌سازی می‌کنند، دامنهٔ گسترده‌ای از Collectionهای CLI را شامل می‌شوند. همانطور که در نمودار زیر نیز می‌توانید مشاهده کنید IEnumerable (از نوع غیر Generic آن) در بالای سلسله مراتب اینترفیس‌های Collectionهای CLI قرار دارد: 

درخت اینترفیس‌های Collectionها در سی‌شارپ

درخت اینترفیس‌های Collectionها در CLI منبع

IEnumerableها همچنین دارای اهمیت دیگری نیز هستند؛ قابلیت‌های LINQ که از دات‌نت ۳.۵ به دات‌نت اضافه شدند به‌عنوان Extensionهای این اینترفیس تعریف شده‌اند و پیاده‌سازی Linq to Objects را می‌توانید در کلاس استاتیک System.Linq.Enumerable در System.Core مشاهده کنید. (می‌توانید برای دیدن آن را با ILDasm یا Reflector باز کنید یا پیاده‌سازی آزاد آن در پروژهٔ Mono را اینجا مشاهده کنید که برای شناخت بیشتر LINQ واقعاً مفید است.)

همچنین این Enumerableها هستند که foreach را امکان‌پذیر می‌کنند. به عبارتی دیگر هر شئ‌ای که قرار باشد در foreach (var x in object) قرار بگیرد و بدین طریق اشیاء درونی‌اش را برای پیمایش یا عملی خاص قرار دهد باید Enumerable باشد.

همانطور که قبلاً هم اشاره شد IEnumerable از نوع غیر Generic در بالای نمودار Collectionها قرار دارد و حتی IEnumerable از نوع Generic نیز باید آن را پشتیبانی کند. این موضوع به احتمال به این دلیل در طراحی لحاظ شد که مهاجرت به .NET 2.0 که قابلیت‌های Generic را افزوده بود ساده‌تر کند. IEnumerable همچنین قابلیت covariance که از قابلیت‌های جدید C# 4.0 هست را دارا است (در اصل IEnumerable دارای Generic از نوع out است).

Enumerableها همانطور که از اسم اینترفیس IEnumerable انتظار می‌رود اشیایی هستند که می‌توانند یک شئ Enumerator که IEnumerator را پیاده‌سازی کرده‌است را از خود ارائه دهند. پس طبیعی است برای فهم و درک دلیل وجودی  Enumerable باید Enumerator را بررسی کنیم.

Enumeratorها

Enumerator شئ است که در یک پیمایش یا به‌عبارت دیگر گذر از روی تک‌تک عضوها ایجاد می‌شود که با حفظ موقعیت فعلی و پیمایش امکان ادامهٔ پیمایش را برای ما فراهم می‌آورد. اگر بخواهید آن را در حقیقت بازسازی کنید شئ Enumerator به‌مانند کاغذ یا جسمی است که بین صفحات یک کتاب قرار می‌دهید که مکانی که در آن قرار دارید را گم نکنید؛ در این مثال، Enumerable همان کتاب است که قابلیت این را دارد که برای پیمایش به وسیلهٔ قرار دادن یک جسم در وسط آن را دارد.

حال برای اینکه دید بهتری از رابطهٔ بین Enumerable و Enumerator از نظر برنامه‌نویسی به این موضوع پیدا کنیم یک کد نمونهٔ عملی را بررسی می‌کنیم.

در اینجا نمونهٔ ساده و خوانایی از استفاده از یک List برای پیشمایش تمامی اعداد قرار دارد:

List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
foreach (int i in list) 
{
    Console.WriteLine(i);
}

همانطور که قبلاً اشاره foreach نیاز به یک Enumerable دارد و List هم با پیاده‌سازی IList که گسترشی از IEnumerable  هست نیز یک نوع Enumerable هست. اگر این کد را Compile کنیم و IL آن را بررسی کنیم متوجه می‌شویم که CLI در اصل چنین کدی را برای اجرا می‌بینید:

List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
IEnumerator<int> listIterator = list.GetEnumerator();
while (listIterator.MoveNext())
{
    Console.WriteLine(listIterator.Current);
}
listIterator.Dispose();

(می‌توان از using استفاده نمود که Dispose را خود انجام دهد که اینجا برای سادگی استفاده نشده‌است.)

همانطور که می‌بینیم یک Enumerator برای Enumerable ما (یعنی List) ایجاد شد و پس از آن با پرسش این موضوع که آیا این پیمایش امکان ادامه دارد، کل اعضا پیموده‌شده و عمل مورد نظر ما بر آن‌ها انجام شده‌است.

خب، تا اینجای کار با خصوصیات و اهمیت  Enumerator‌ها و  Enumerable‌ها آشنا شدیم، حال نوبت به آن می‌رسد که بررسی کنیم آن‌ها را چگونه می‌سازند و بعد از آن با کاربردهای فراتری از آن‌ها نسبت به پیمایش یک  List آشنا شویم.

ساخت Enumeratorها و Enumerableها

همانطور که اشاره شد ایجاد اشیاء Enumerable به اشیاء Enumerator مربوط است، پس ما در یک قطعه کد که پیمایش از روی یک آرایه را فراهم می‌آورد ایجاد هر دوی آن‌ها و رابطهٔ بینشان را بررسی می‌کنیم.

    public class ArrayEnumerable<T> : IEnumerable<T>
    {
        private T[] _array;
        public ArrayEnumerable(T[] array)
        {
            _array = array;
        }


        public IEnumerator<T> GetEnumerator()
        {
            return new ArrayEnumerator<T>(_array);
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    public class ArrayEnumerator<T> : IEnumerator<T>
    {
        private T[] _array;
        public ArrayEnumerator(T[] array)
        {
            _array = array;
        }

        public int index = -1;

        public T Current { get { return _array[index]; } }

        object System.Collections.IEnumerator.Current { get { return this.Current; } }

        public bool MoveNext()
        {
            index++;
            return index < _array.Length;
        }

        public void Reset()
        {
            index = 0;
        }

        public void Dispose() { }
    }

ادامه

اشتراک‌ها
ایجاد فایل PDF در #C

The PDF File Writer C# class library PdfFileWriter allows you to create PDF files directly from your .net application.
Most TrueType fonts such as Arial supports character values greater than 255. The PDF File Library allows you to perform a substitution. You can use any Unicode character and map it into the available one byte range.

ایجاد فایل PDF در #C
مطالب
استفاده از LocalDb در IIS، قسمت اول: پروفایل کاربر
این مقاله قسمت اول یک سری دو قسمتی است، که در آن به نحوه استفاده از LocalDB در IIS می‌پردازیم.

LocalDb دیتابیس توصیه شده برای ویژوال استودیو است و برای انواع پروژه‌ها مانند اپلیکیشن‌های وب می‌تواند استفاده شود. هنگام استفاده از این دیتابیس در IIS Express یا Cassini همه چیز طبق انتظار کار میکند. اما به محض آنکه بخواهید از آن در Full IIS استفاده کنید با خطاهایی مواجه می‌شوید. مقصود از Full IIS همان نسخه ای است که بهمراه ویندوز عرضه می‌شود و در قالب یک Windows Service اجرا می‌گردد.

هنگام استفاده از Full IIS دو خاصیت از LocalDb باعث بروز مشکل می‌شوند:

  • LocalDb نیاز دارد پروفایل کاربر بارگذاری شده باشد
  • بصورت پیش فرض، وهله LocalDb متعلق به یک کاربر بوده و خصوصی است

در ادامه این مقاله روی بارگذاری پروفایل کاربر تمرکز می‌کنیم، و در قسمت بعدی به مالکیت وهله LocalDb می‌پردازیم.


بارگذاری پروفایل کاربر

بگذارید دوباره به خطای موجود نگاهی بیاندازیم:

System.Data.SqlClient.SqlException: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 0 - [x89C50120]) 

این پیغام خطا زیاد مفید نیست، اما LocalDb اطلاعات بیشتری در Event Log ویندوز ذخیره می‌کند. اگر Windows Logs را باز کنید و به قسمت Application بروید پیغام زیر را مشاهده خواهید کرد.

Windows API call SHGetKnownFolderPath returned error code: 5. Windows system error message is: Access is denied
Reported at line: 400

بعلاوه این پیام خطا:

Cannot get a local application data path. Most probably a user profile is not loaded. If LocalDB is executed under IIS, make sure that profile loading is enabled for the current user. 


به احتمال زیاد تعداد بیشتری از این دو خطا در تاریخچه وقایع وجود خواهد داشت، چرا که منطق کانکشن ADO.NET چند بار سعی می‌کند در بازه‌های مختلف به دیتابیس وصل شود.

پیغام خطای دوم واضح است، نیاز است پروفایل کاربر را بارگذاری کنیم. انجام اینکار زیاد مشکل نیست، هر Application Pool در IIS تنظیماتی برای بارگذاری پروفایل کاربر دارد که از قسمت Advanced Settings قابل دسترسی است. متاسفانه پس از انتشار سرویس پک 1 برای Windows 7 مسائل کمی پیچیده‌تر شد. در حال حاظر فعال کردن loadUserProfile برای بارگذاری کامل پروفایل کاربر به تنهایی کافی نیست، و باید setProfileEnvironment را هم فعال کنیم. برای اطلاعات بیشتر در این باره به مستندات KB 2547655 مراجعه کنید. بدین منظور باید فایل applicationHost.config را ویرایش کنید. فایل مذکور در مسیر C:\Windows\System32\inetsrv\config قرار دارد. همانطور که در مستندات KB 2547655 توضیح داده شده، باید پرچم هر دو تنظیمات را برای ASP.NET 4.0 فعال کنیم:

 <add name="ASP.NET v4.0" autoStart="true" managedRuntimeVersion="v4.0" managedPipelineMode="Integrated">
    <processModel identityType="ApplicationPoolIdentity" loadUserProfile="true" setProfileEnvironment="true" />
</add>
پس از بروز رسانی این تنظیمات، Application Pool را Restart کنید و اپلیکیشن را مجددا اجرا نمایید. اگر همه چیز طبق انتظار پیش برود، با خطای جدیدی مواجه خواهیم شد:

جای هیچ نگرانی نیست، چرا که این پیغام خطا انتظار می‌رود. همانطور که در ابتدا گفته شد، دو خاصیت LocalDb باعث بروز این خطاها می‌شوند و ما هنوز به خاصیت دوم نپرداخته ایم. بصورت پیش فرض وهله‌های LocalDb خصوصی (private) هستند و در Windows account جاری اجرا می‌شوند. بنابراین ApplicationPoolIdentity در IIS به وهله‌های دیتابیس دسترسی نخواهد داشت. در قسمت دوم این مقاله، راه‌های مختلفی را برای رفع این مشکل بررسی می‌کنیم.

مطالب
راه اندازی دیتابیس postgresql در برنامه‌های ASP.NET Core – قسمت 2

در قسمت قبل به معرفی postgresql پرداختیم; در این قسمت قصد ایجاد و راه اندازی یک api با استفاده از دیتابیس postgresql و استفاده از تکنولوژی‌های آن را با استفاده از docker داریم.


ابتدا با استفاده از دستور زیر یک پروژه‌ی جدید asp.net core را ایجاد کنید:

dotnet new webapi --minimal -o YourDirectoryPath:\YourFolderName

سپس فایل docker-compose.yaml را به روت پروژه اضافه کنید که شامل کانفیگ‌های زیر میباشد: 

version: '3.1'

services:

  db:
    image: postgres
    container_name: db
    restart: always
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USERNAME: postgres
      POSTGRES_DB: BloggingDb
    ports:
        - "5432:5432"
    volumes:
      - postgres_data:/data/db

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
  
  pgadmin4:
    image: dpage/pgadmin4
    restart: always
    environment:
      PGADMIN_DEFAULT_EMAIL: pgadmin4@pgadmin.org
      PGADMIN_DEFAULT_PASSWORD: admin
      PGADMIN_CONFIG_SERVER_MODE: 'False'
    ports:
      - 5050:80
    volumes:
      - pgadmin:/var/lib/pgadmin
    depends_on:
      - db

volumes:
  postgres_data:
  pgadmin:

سپس با اجرای دستور زیر در روت پروژه، سرویس‌ها را راه اندازی کنید: 

docker compose up -d


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

سرویس db

نمونه ایمیج اصلی، volume، تنظیمات connection string در آن استفاده شده است.

سرویس adminer :

https://hub.docker.com/_/adminer /

Adminer - Database management in a single PHP file

یک برنامه تحت وب مدیریت پایگاه داده ساده میباشد که ویژگی‌ها MySql را در کنار سرعت و امنیت ارائه میدهد و در آدرس http://localhost:8080 / اجرا خواهد شد.

سرویس pgadmin4 :

pgAdmin - PostgreSQL Tools

dpage/pgadmin4 - Docker Image | Docker Hub

در حال حاضر این برنامه محبوب‌ترین برنامه مدیریت پایگاه داده میباشد که ویژگی‌های پیشرفته‌ای را نیز پوشش میدهد و در آدرس http://localhost:5050 / اجرا خواهد شد. 


اکنون نوبت نوشتن کد‌ها میباشد. 

- تنظیم connection string در فایل appsettings.json:

"ConnectionStrings": {
    "BloggingContext": "Username=postgres;Password=postgres;Server=localhost;Database=BloggingDb”
}

- و همینطور پکیج‌های زیر را به برنامه خود رفرنس دهید: 

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design

- مدل‌های برنامه را در مسیر /Models ایجاد کنید: 

namespace NpgsqlAPI.Models;

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; } = null!;
    public string Content { get; set; } = null!;

    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
}

namespace NpgsqlAPI.Models;

public class Blog
{
    public int BlogId { get; set; }
    public string? Url { get; set; }

    public List<Post>? Posts { get; set; }
}

- سپس BloggingContext را در مسیر /Data ایجاد کنید:

using Microsoft.EntityFrameworkCore;
using NpgsqlAPI.Models;

namespace NpgsqlAPI.Data;

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
    }
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Post


- سپس اینترفیس IBlogServices را در مسیر  /Servicec/Blogs ایجاد کنید: 

using NpgsqlAPI.Models;

namespace NpgsqlAPI.Services.Blogs;
public interface IBlogServices
{
    Task<IEnumerable<Blog>> GetList();
    Task<Blog?> Get(uint id);
    Task<uint> Add(Blog obj);
    Task AddRange(Blog[] obj);
    Task Update(Blog obj);
    Task UpdateRange(Blog[] obj);
    Task Remove(uint id);
}


-  و سپس پیاده سازی آن را در فایل BlogEFServices و در کنار اینترفیس آن قرار دهید: 

using Microsoft.EntityFrameworkCore;
using NpgsqlAPI.Data;
using NpgsqlAPI.Models;

namespace NpgsqlAPI.Services.Blogs;
public sealed class BlogEFServices : IBlogServices
{
    private readonly BloggingContext _context;
    public BlogEFServices(BloggingContext context)
    {
        _context = context;
    }

    public async Task<uint> Add(Blog obj)
    {
        await _context.Blogs.AddAsync(obj);
        return (uint)await SaveChangesAsync();
    }

    public async Task AddRange(Blog[] obj)
    {
        await _context.Blogs.AddRangeAsync(obj);
        await SaveChangesAsync();
    }

    public async Task<Blog?> Get(uint id)
    {
        return await _context.Blogs.FirstOrDefaultAsync(x=>x.BlogId == id);
    }

    public async Task<IEnumerable<Blog>> GetList()
    {
       return await _context.Blogs.ToListAsync();
    }

    public async Task Remove(uint id)
    {
        var entity = await Get(id);
        _context.Blogs.Remove(entity!);
        await SaveChangesAsync();
    }

    public async Task Update(Blog obj)
    {
        _context.Blogs.Update(obj);
        await SaveChangesAsync();
    }

    public async Task UpdateRange(Blog[] obj)
    {
        _context.Blogs.UpdateRange(obj);
        await SaveChangesAsync();
    }

    private async Task<int> SaveChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }
}


- اکنون  endpoint‌های api را در فایل program.cs ایجاد کنید:

using System.Data;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using NpgsqlAPI.Services.Blogs;
using NpgsqlAPI.Data;
using NpgsqlAPI.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

string connectionString = builder.Configuration.GetConnectionString("BloggingContext")!;

builder.Services.AddDbContext<BloggingContext>(options =>
        options.UseNpgsql(connectionString));

builder.Services.AddTransient<IDbConnection>(_ => new NpgsqlConnection(connectionString));

// builder.Services.AddScoped<IBlogServices, BlogDapperServices>();
// builder.Services.AddScoped<IBlogServices, BlogEFRawQueryServices>();
builder.Services.AddScoped<IBlogServices, BlogEFServices>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.MapGet("/blogs", async (IBlogServices service) => await service.GetList())
.WithName("GetBlogs")
.WithOpenApi();

app.MapGet("/blogs/{id}", async (IBlogServices service, uint id) => await service.Get(id))
.WithName("GetBlog")
.WithOpenApi();

app.MapPost("/blogs", async (IBlogServices service, Blog blog) => await service.Add(blog))
.WithName("AddBlog")
.WithOpenApi();

app.MapDelete("/blogs/{id}", async (IBlogServices service, uint id) => await service.Remove(id))
.WithName("RemoveBlog")
.WithOpenApi();

app.MapPut("/blogs", async (IBlogServices service, Blog blog) => await service.Update(blog))
.WithName("UpdateBlog")
.WithOpenApi();

app.MapPut("/blogs/Bulk", async (IBlogServices service, Blog[] blogs) =>
 await service.UpdateRange(blogs))
.WithName("UpdateBulkBlog")
.WithOpenApi();

app.MapPost("/blogs/Bulk", async (IBlogServices service, Blog[] blogs) =>
 await service.AddRange(blogs))
.WithName("AddBulkBlog")
.WithOpenApi();

app.Run();

تمامی کد‌های برنامه تا به اینجا نوشته شده‌اند. اکنون migration را پس از اطمینان از اجرا بودن داکر اجرا کنید 

dotnet ef migrations add Init
dotnet ef database update

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


کد‌های کامل این مطلب

مطالب
طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD

هدف، ارائه راه‌حلی برای نمایش جدولی اطلاعات، جستجو، مرتب سازی و صفحه بندی و همچنین انجام عملیات ثبت، ویرایش و حذف بر روی آنها به صورت Ajaxای در بخش back office نرم افزار می‌باشد.

پیش نیازها:

ایده کار به این شکل می‌باشد که برای نمایش اطلاعات به صورت جدولی با قابلیت‌های مذکور، لازم است یک اکشن Index برای نمایش اولیه و صفحه اول اطلاعات صفحه بندی شده و اکشن متدی به نام List برای پاسخ به درخواست‌های صفحه بندی، مرتب سازی، تغییر تعداد آیتم‌ها در هر صفحه و همچنین جستجو، داشته باشیم که این اکشن متد List، بعد از واکشی اطلاعات مورد نظر از منبع داده، آنها را به همراه اطلاعاتی که در کوئری استرینگ درخواست جاری وجود دارد در قالب یک PartialView به کلاینت ارسال کند.


ایجاد مدل‌های پایه

همانطور که در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» مطرح شد، برای پیاده سازی متدهای GetPagedList در ApplicationService‌ها از الگوی Request/Response استفاده می‌کنیم. برای این منظور واسط و کلاس‌های زیر را خواهیم داشت:

واسط IPagedQueryModel

    public interface IPagedQueryModel
    {
        int Page { get; set; }
        int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        string SortExpression { get; set; }
    }

این واسط قراردادی می‌باشد برای نوع و نام پارامترهایی که توسط کلاینت به سرور ارسال می‌شود. پراپرتی SortExpression آن، نام و ترتیب مرتب سازی را مشخص می‌کند؛ برای این منظور FieldName_ASC و FieldName_DESC به ترتیب برای حالات مرتب سازی صعودی و نزولی براساس FieldName مقدار دهی خواهد شد.

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

  public class PagedQueryModel : IPagedQueryModel, IShouldNormalize
    {
        public int Page { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        public string SortExpression { get; set; }

        public virtual void Normalize()
        {
            if (Page < 1)
                Page = 1;

            if (PageSize < 1)
                PageSize = 10;

            if (SortExpression.IsEmpty())
                SortExpression = "Id_DESC";
        }
    }

مدل بالا علاوه بر پیاده سازی واسط IPagedQueryModel، پیاده ساز واسط IShouldNormalize نیز می‌باشد؛ دلیل وجود چنین واسطی در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» توضیح داده شده است:

پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.

کلاس PagedQueryResult

    public class PagedQueryResult<TModel>
    {
        public PagedQueryResult()
        {
            Items = new List<TModel>();
        }
        public IEnumerable<TModel> Items { get; set; }
        public long TotalCount { get; set; }
    }

دلیل وجود کلاس بالا در مقاله «طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور» توضیح داده شده است:

عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.

کلاس PagedListModel

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

    public class PagedListModel<TModel>
    {
        public IPagedQueryModel Query { get; set; }

        public PagedQueryResult<TModel> Result { get; set; }
    }

پراپرتی Query در برگیرنده پارامتر ورودی اکشن متد List می‌باشد که پراپرتی‌های آن با مقادیر موجود در کوئری استرینگ درخواست جاری مقدار دهی شده‌اند؛ البته بدون وجود کلاس بالا نیز به کمک ViewBag می‌شود این اطلاعات ترکیبی را به ویو ارسال کرد که پیشنهاد نمی‌شود.


متد GetPagedListAsync موجود در CrudApplicationService

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedQueryModel, TDynamicQueryModel> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel>
        where TEntity : Entity, new()
        where TCreateModel : class
        where TEditModel : class, IModel
        where TModel : class, IModel
        where TDeleteModel : class, IModel
        where TPagedQueryModel : PagedQueryModel, new()
        where TDynamicQueryModel : DynamicQueryModel

    {

        #region Properties

        protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking();
        public IUnitOfWork UnitOfWork { get; set; }
        public IMapper Mapper { get; set; }
        protected IDbSet<TEntity> EntitySet => UnitOfWork.Set<TEntity>();

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        public virtual async Task<PagedQueryResult<TModel>> GetPagedListAsync(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var query = ApplyFiltering(model);

            var totalCount = await query.LongCountAsync().ConfigureAwait(false);

            var result = query.ProjectTo<TModel>(Mapper.ConfigurationProvider);

            result = result.ApplySorting(model);
            result = result.ApplyPaging(model);

            return new PagedQueryResult<TModel>
            {
                Items = await result.ToListAsync().ConfigureAwait(false),
                TotalCount = totalCount
            };
        }
        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            return UnTrackedEntitySet;
        }
        #endregion
    }

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

    public static class QueryableExtensions
    {
        public static IQueryable<TModel> ApplySorting<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return query.OrderBy(request.SortExpression.Replace('_', ' '));
        }

        public static IQueryable<TModel> ApplyPaging<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return request != null
                ? query.Page((request.Page - 1) * request.PageSize, request.PageSize)
                : query;
        }
    }

به منظور مرتب سازی از کتابخانه  System.Liq.Dynamic کمک گرفته شده‌است.

نکته: مشخص است که این روش، وابستگی به وجود متد GetPagedListAsync ندارد و صرفا برای تشریح ارتباط مطالبی که قبلا منتشر شده بود، مطرح شد.


پیاده سازی اکشن متدهای Index و List

public partial class RolesController : BaseController
{
    #region Fields
        private readonly IRoleService _service;
        private readonly ILookupService _lookupService;

        #endregion

    #region Constractor
        public RolesController(IRoleService service,  ILookupService lookupService)
        {
            Guard.ArgumentNotNull(service, nameof(service));
            Guard.ArgumentNotNull(lookupService, nameof(lookupService));

            _service = service;
            _lookupService = lookupService;
        }
        #endregion

    #region Index / List
    [HttpGet]
    public virtual async Task<ActionResult> Index()
    {
        var query = new RolePagedQueryModel();
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var pagedList = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        var model = new RoleIndexViewModel
        {
            PagedListModel = pagedList,
            Permissions = _lookupService.GetPermissions()
        };
        return View(model);
    }
    [HttpGet, AjaxOnly, NoOutputCache]
    public virtual async Task<ActionResult> List(RolePagedQueryModel query)
    {
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var model = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        return PartialView(MVC.Administration.Roles.Views._List, model);
    }
    #endregion
}

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

    public class RolePagedQueryModel : PagedQueryModel
    {
        public string Name { get; set; }
        public string Permission { get; set; }
    }

در این مورد خاص لازم است لیست دسترسی‌های موجود درسیستم به صورت لیستی برای انتخاب در فرم جستجو مهیا باشد. فرم جستجو در ویو مربوط به اکشن Index قرار می‌گیرد و قرار نیست به همراه پارشال ویو List_ در هر درخواستی از سرور دریافت شود. لذا لازم است مدلی برای ویو Index در نظر بگیریم که به شکل زیر می‌باشد:

    public class RoleIndexViewModel
    {   
        public RoleIndexViewModel()
        {
            Permissions = new List<LookupItem>();
        }
        public IReadOnlyList<LookupItem> Permissions { get; set; }
        public PagedListModel<RoleModel> PagedListModel { get; set; }
    }

پراپرتی PagedListModel در برگیرنده اطلاعات مربوط به نمایش اولیه جدول اطلاعات می‌باشد و پراپرتی Permissions لیست دسترسی‌های موجود درسیستم را به ویو منتقل خواهد کرد. اگر ویو ایندکس شما به داده اضافه ای نیاز ندارد، از ایجاد مدل بالا صرف نظر کنید.


ویو Index.cshtml

@model RoleIndexViewModel

@{
    ViewBag.Title = L("Administration.Views.Role.Index.Title");
    ViewBag.ActiveMenu = AdministrationMenuNames.RoleManagement;
}

<div class="row">
    <div class="col-md-12">
        <div id="filterPanel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="filterPanel">
            <div class="panel panel-default margin-bottom-5">

                <div class="panel-body">
                    @using (Ajax.BeginForm(MVC.Administration.Roles.List(),
new AjaxOptions { UpdateTargetId = "RolesList", HttpMethod = "GET" }, new { id = "filterForm", data_submit_on_reset = "true" }))
                    {
                        <div class="row">
                            <div class="col-md-3">
                                <input type="text" name="Name" class="form-control" value="" placeholder="@L("Administration.Role.Fields.Name")" />
                            </div>
                            <div class="col-md-3">
                                @Html.DropDownList("Permission", Model.Permissions.ToSelectListItems(), L("Administration.Views.Role.FilterBy.Permission"),new {@class="form-control"})
                            </div>
                            <div class="col-md-3">

                                <button type="submit"
                                        role="button"
                                        class="btn btn-info">
                                    @L("Commands.Filter")
                                </button>
                                <button type="reset"
                                        role="button"
                                        class="btn btn-default">
                                    <i class="fa fa-close"></i>
                                    @L("Commands.Reset")
                                </button>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <div class="col-md-12" id="RolesList">
        @{Html.RenderPartial(MVC.Administration.Roles.Views._List, Model.PagedListModel);}
    </div>
</div>

فرم جستجو باید دارای ویژگی data_submit_on_reset با مقدار "true" باشد. به منظور پاکسازی فرم جستجو و ارسال درخواست جستجو با فرمی خالی از داده، برای بازگشت به حالت اولیه از تکه کد زیر استفاده خواهد شد:

  $(document).on("reset", "form[data-submit-on-reset]",
            function () {
                var form = this;
                setTimeout(function () {
                    $(form).submit();
                });
            });

در ادامه پارشال ویو List_ با داده ارسالی به ویو Index، رندر شده و کار نمایش اولیه اطلاعات به صورت جدولی به اتمام می‌رسد.


پارشال ویو List.cshtml_

@model PagedListModel<RoleModel>
@{
    Layout = null;
    var rowNumber = (Model.Query.Page - 1) * Model.Query.PageSize + 1;
    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));
}

<div class="panel panel-default margin-bottom-5">
    <table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">
        <thead>
            <tr>
                <th style="width: 5%;">
                    #
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("DisplayName", L("Administration.Role.Fields.DisplayName"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("IsDefault", L("Administration.Role.Fields.IsDefault"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
               
                <th style="width: 5%;"></th>
            </tr>
        </thead>

        <tbody>
           @foreach (var role in Model.Result.Items)
            {
                <tr>
                    <td>@(rowNumber++.ToPersianNumbers())</td>
                    <td>@role.Name</td>
                    <td>@role.DisplayName</td>
                    <td class="text-center">@Html.DisplayFor(a => role.IsDefault)</td>
                    <td class="text-center operations">
                      
                        <div class="btn-group">

                            <span class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
                            <ul class="dropdown-menu dropdown-menu-left">
                                <li>
                                    <a href="#"
                                       role="button"
                                       data-ajax="true"
                                       data-ajax-method="GET"
                                       data-ajax-update="#main-modal div.modal-content"
                                       data-ajax-url="@Url.Action(MVC.Administration.Roles.Edit(role.Id))"
                                       data-toggle="modal"
                                       data-target="#main-modal">
                                        <i class="fa fa-pencil"></i>
                                        @L("Commands.Edit")
                                    </a>
                                </li>
                                <li>
                                    <a href="#"
                                       role="button"
                                       id="delete-@role.Id"
                                       data-delete-url="@Url.Action(MVC.Administration.Roles.Delete())"
                                       data-delete-model='{"Id":"@role.Id","RowVersion":"@Convert.ToBase64String(role.RowVersion)"}'>
                                        <i class="fa fa-trash"></i>
                                        @L("Commands.Delete")
                                    </a>
                                </li>
                            </ul>
                        </div>
                    </td>
                </tr>
            }
        </tbody>
    </table>

</div>

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>

به ترتیب  فایل بالا را بررسی می‌کنیم:

    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));

refreshUrl برای ارسال درخواست به اکشن متد List در نظر گرفته شده‌است که در کوئری استرینگ مربوط به خود، اطلاعاتی (مرتب سازی، شماره صفحه، اطلاعات جستجو و همچنین تعداد آیتم‌های موجود در هر صفحه) را دارد که حالت فعلی گرید را می‌توانیم دوباره از سرور درخواست کنیم.

<table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">

دو ویژگی data-ajax-refresh-url و data-ajax-refresh-update برای جدولی که لازم است عملیات CRUD را پشتیبانی کند، لازم می‌باشد. در قسمت دوم به استفاده از این دو ویژگی در هنگام عملیات ثبت، ویرایش و حذف خواهیم پرداخت.

<th class="col-md-3 sortable">
    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
</th>

ستونی که امکان مرتب سازی را دارد باید th آن، کلاس sortable را داشته باشد. همچنین باید از هلپری که پیاده سازی آن را در ادامه خواهیم دید، استفاده کنیم. این هلپر، نام فیلد، عنوان ستون، مدل Query و همچین یک urlFactory را در قالب یک ‎Func<RouteValueDictionary,string>‎ دریافت می‌کند.


پیاده سازی هلپر SortableColumn

        public static MvcHtmlString SortableColumn(this HtmlHelper html, string columnName,
            string columnDisplayName, IPagedQueryModel queryModel, string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
        {
            var dictionary = queryModel.ToDictionary();

            var routeValueDictionary = new RouteValueDictionary(dictionary)
            {
                ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
                    ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
                        ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                            ? string.Empty : $"{columnName}_DESC"
            };

            var url = urlFactory(routeValueDictionary);

            var aTag = new TagBuilder("a");
            aTag.Attributes.Add("href", "#");
            aTag.Attributes.Add("data-ajax", "true");
            aTag.Attributes.Add("data-ajax-method", "GET");
            aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
            aTag.Attributes.Add("data-ajax-url", url);
            aTag.InnerHtml = columnDisplayName;

            var iconCssClass = !queryModel.SortExpression.StartsWith(columnName)
                ? "fa-sort"
                : queryModel.SortExpression.EndsWith("DESC")
                    ? "fa-sort-down"
                    : "fa-sort-up";

            var iTag = new TagBuilder("i");
            iTag.AddCssClass($"fa {iconCssClass}");

            return new MvcHtmlString($"{aTag}\n{iTag}");
        }

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

public static IDictionary<string, object> ToDictionary(this object source)
{
    return source.ToDictionary<object>();
}

public static IDictionary<string, T> ToDictionary<T>(this object source)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    var dictionary = new Dictionary<string, T>();

    foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source))
    {
        AddPropertyToDictionary(property, source, dictionary);
    }
    return dictionary;
}

private static void AddPropertyToDictionary<T>(PropertyDescriptor property, object source,
    IDictionary<string, T> dictionary)
{
    var value = property.GetValue(source);

    var items = value as IEnumerable;

    if (items != null && !(items is string))
    {
        var i = 0;
        foreach (var item in items)
        {
            dictionary.Add($"{property.Name}[{i++}]", (T)item);
        }
    }
    else if (value is T)
    {
        dictionary.Add(property.Name, (T)value);
    }

}

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

در ادامه پیاده سازی هلپر SortableColumn، از دیکشنری حاصل، یک وهله از RouteValueDictionary ساخته می‌شود. در زمان رندر شدن PartialView لازم است مشخص شود که برای دفعه بعدی که بر روی این ستون کلیک می‌شود، باید چه مقداری با پارامتر SortExpression موجود در کوئری استرینگ ارسال شود. از این جهت برای پشتیبانی ستون، از حالت‌های مرتب سازی صعودی، نزولی و برگشت به حالت اولیه بدون مرتب سازی، کد زیر را خواهیم داشت:

var routeValueDictionary = new RouteValueDictionary(dictionary)
{
    ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
        ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
            ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                ? string.Empty : $"{columnName}_DESC"
};

در ادامه urlFactory با routeValueDictionary حاصل، Invoke می‌شود تا url نهایی برای مرتب سازی‌های بعدی را  از طریق یک لینک تزئین شده با data اتریبیوت‌های Unobtrusive Ajax در th مربوطه قرار دهیم.

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

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>


پیاده سازی هلپر Pager

public static MvcHtmlString Pager<TModel>(this HtmlHelper html, PagedListModel<TModel> model,
        string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
{
    return html.PagedListPager(
        new StaticPagedList<TModel>(model.Result.Items, model.Query.Page, model.Query.PageSize,
            (int)model.Result.TotalCount), page =>
       {
           var dictionary = model.Query.ToDictionary();
           var routeValueDictionary = new RouteValueDictionary(dictionary) { ["Page"] = page };
           return urlFactory(routeValueDictionary);
       }, PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing(
            new PagedListRenderOptions
            {
                DisplayLinkToFirstPage = PagedListDisplayMode.Always,
                DisplayLinkToLastPage = PagedListDisplayMode.Always,
                DisplayLinkToPreviousPage = PagedListDisplayMode.Always,
                DisplayLinkToNextPage = PagedListDisplayMode.Always,
                MaximumPageNumbersToDisplay = 6,
                DisplayItemSliceAndTotal = true,
                DisplayEllipsesWhenNotShowingAllPageNumbers = true,
                ItemSliceAndTotalFormat = $"تعداد کل: {model.Result.TotalCount.ToPersianNumbers()}",
                FunctionToDisplayEachPageNumber = page => page.ToPersianNumbers(),
            },
            new AjaxOptions
            {
                AllowCache = false,
                HttpMethod = "GET",
                InsertionMode = InsertionMode.Replace,
                UpdateTargetId = updateTargetId
            }));
}

در متد بالا از کتابخانه PagedList.Mvc استفاده شده‌است. یکی از overload‌های متد PagedListPager آن، یک پارامتر از نوع Func<int, string>‎ به نام generatePageUrl را دریافت می‌کند که امکان شخصی سازی فرآیند تولید لینک به صفحات بعدی و قبلی را به ما می‌دهد. ما نیز از این امکان برای افزودن اطلاعات موجود در مدل Query، به کوئری استرینگ لینک‌های تولیدی استفاده کردیم و صرفا برای لینک‌های ایجادی لازم بود مقادیر پارامتر Page موجود در کوئری استرینگ تغییر کند که در کد بالا مشخص می‌باشد.


پیاده سازی هلپر PageSize

public static MvcHtmlString PageSize(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, object htmlAttributes = null, string filterFormId = null, params int[] numbers)
{
    if (numbers.Length == 0)
        numbers = new[] { 10, 20, 30, 50, 100 };

    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary)
    {
        [nameof(IPagedQueryModel.Page)] = 1
    };
    routeValueDictionary.Remove(nameof(IPagedQueryModel.PageSize));

    var url = urlFactory(routeValueDictionary);

    var formTag = new TagBuilder("form");
    formTag.Attributes.Add("action", url);
    formTag.Attributes.Add("method", "GET");
    formTag.Attributes.Add("data-ajax", "true");
    formTag.Attributes.Add("data-ajax-method", "GET");
    formTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    formTag.Attributes.Add("data-ajax-url", url);

    if (htmlAttributes != null)
        formTag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));

    formTag.AddCssClass("form-inline inline");

    var items = numbers.Select(number =>
        new SelectListItem
        {
            Value = number.ToString(),
            Text = number.ToString().ToPersianNumbers(),
            Selected = queryModel.PageSize == number
        });

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

    if (filterFormId.IsEmpty()) return new MvcHtmlString($"{formTag}");

    // ReSharper disable once MustUseReturnValue
    var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";

    return new MvcHtmlString($"{formTag}\n{scriptBlock}");
}

ایده کار به این صورت است که یک المنت select، درون یک المنت form قرار می‌گیرد و در زمان change آن، فرم مربوطه submit می‌شود.

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

در زمان تغییر تعداد نمایشی آیتم‌ها در هر صفحه، لازم است حالت فعلی گرید حفظ شود و صرفا پارامتر Page ریست شود.


نکته مهم: در این طراحی اگر فرم جستجویی دارید، در زمان جستجو هیچیک از پارامتر‌های مربوط به صفحه بندی و مرتب سازی به سرور ارسال نخواهند شد (در واقع ریست می‌شوند) و کافیست یک درخواست GET معمولی با ارسال محتویات فرم به سرور صورت گیرد؛ ولی لازم است PageSize تنظیم شده، در زمان اعمال فیلتر نیز به سرور ارسال شود. از این جهت اسکریپتی برای ایجاد یک input مخفی در فرم جستجو نیز هنگام رندر شدن PartialView در صفحه تزریق می‌شود.

  var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";


پیاده سازی هلپر Refresh

public static MvcHtmlString Refresh(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel,
    Func<RouteValueDictionary, string> urlFactory, string label = null)
{
    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary);

    var url = urlFactory(routeValueDictionary);

    var aTag = new TagBuilder("a");
    aTag.Attributes.Add("href", "#");
    aTag.Attributes.Add("role", "button");
    aTag.Attributes.Add("data-ajax", "true");
    aTag.Attributes.Add("data-ajax-method", "GET");
    aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    aTag.Attributes.Add("data-ajax-url", url);
    aTag.AddCssClass("btn btn-default");

    var iTag = new TagBuilder("i");
    iTag.AddCssClass("fa fa-refresh");

    aTag.InnerHtml = $"{iTag} {label}";

    return new MvcHtmlString(aTag.ToString());
}

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


روش دیگر برای مدیریت این چنین کارهایی، استفاده از یک المنت form و قرادادن کل گرید به همراه یک سری input مخفی معادل با پارامترهای دریافتی اکشن متد List و مقدار دهی آنها در زمان کلیک بر روی دکمه‌های صفحه بندی، بارگزاری مجدد، دکمه اعمال فیلتر و لیست آبشاری تنظیم تعداد آیتم‌ها، درون آن نیز میتواند کار ساز باشد؛ اما در زمان پیاده سازی خواهید دید که پیاده سازی آن خیلی سرراست، به مانند پیاده سازی موجود در مطلب جاری نخواهد بود. 

در قسمت دوم، به پیاده سازی عملیات ثبت، ویرایش و حذف برپایه مودال‌های بوت استرپ و افزونه Unobtrusive Ajax خواهیم پرداخت.
کدهای کامل قسمت جاری، بعد از انتشار قسمت دوم، در مخزن گیت هاب شخصی قرار خواهد گرفت.

مطالب
استفاده از پلاگین DataTables کتابخانه jQuery در برنامه‌های ASP.NET Core
datatable js، کتابخانه‌ای جهت ساخت جداول است و نسبت به رقیب اصلی خودش یعنی kendo telerik، از سادگی بیشتری برخوردار هست و امکانات خوبی هم دارد.

اگر برای جداول صفحات خود، از کتابخانه‌ی جی‌کوئری datatable استفاده میکنید، بعد از مدتی که تعداد رکورد‌ها زیاد میشوند، شاهد کند شدن صفحه خود خواهید شد. برای رفع این مشکل نیاز به پیاده سازی pagination دارید که به صورت خیلی ساده‌ای قابل پیاده سازی هست و شما تغییر کمی را در سمت سرور اعمال میکنید و سایر موارد توسط خود کتابخانه انجام میشود.

در ابتدا به بررسی کد‌ها و تغییرات سمت فرانت‌اند و صفحه‌ی cshtml می‌پردازیم:
1- تابع Ajax ای که وظیفه‌ی دریافت اطلاعات را دارد، به کل پاک کنید. چون Ajax به صورت یک آبجکت، به درون خود دیتاتیبل منتقل خواهد شد.
2- در صفحه خود، کد زیر را قرار دهید (جهت جلوگیری از 400 bad request) که این کار فقط برای هندلر‌های razor page و یا controller نیاز است و اگر از API استفاده میکنید، مسلما نیازی به این مدل تنظیمات نیست.
@Html.AntiForgeryToken()
3- سپس کد زیر را به startup خود اضافه کنید (در قسمتی که دارید اینترفیس‌ها را ثبت میکنید):
//Post in Ajax
services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
4- حالا نوبت کانفیگ‌های دیتاتیبل هست:
function initDataTables() {
        table.destroy();
        table = $("#tblJs").DataTable({
          processing: true,
          serverSide: true,
          filter: true,
          ajax: {
            url: '@Url.Page("yourPage","yourHandler")',
            beforeSend: function (xhr) {
              xhr.setRequestHeader("XSRF-TOKEN",
                $('input:hidden[name="__RequestVerificationToken"]').val());
            },
            type: "POST",
            datatype: "json"
          },
          language: {
            url: "/Persian.json"
          },
          responsive: true,
          select: true,

          columns: scheme,
          select: true,
        });
      }
5-کد بالا، کل تابعی را نشان میدهد که وظیفه‌ی ساخت دیتاتیبل را دارد؛ ولی شما تنها نیاز دارید قسمت زیر را اضافه کنید:
processing: true,
          serverSide: true,
          filter: true,
          ajax: {
            url: '@Url.Page("yourPage","yourHandler  ")',
            beforeSend: function (xhr) {
              xhr.setRequestHeader("XSRF-TOKEN",
                $('input:hidden[name="__RequestVerificationToken"]').val());
            },
            type: "POST",
            datatype: "json"
          },

حالا باید کد‌های سمت سرور را بنویسیم. برای این منظور باید ابتدا مقادیری را که دیتاتیبل برای ما ارسال میکند، از ریکوئست دریافت کنیم.
6- کل دیتایی که دیتا تیبل برای ما میفرستد، به مدل زیر خلاصه میشود:
public class FiltersFromRequestDataTable
    {
        public string length { get; set; }
        public string start { get; set; }
        public string sortColumn { get; set; }
        public string sortColumnDirection { get; set; }
        public string sortColumnIndex { get; set; }
        public string draw { get; set; }
        public string searchValue { get; set; }
        public int pageSize { get; set; }
        public int skip { get; set; }
    }
نکته‌ی مهم این است که پراپرتی‌ها باید با اسم کوچک به سمت فرانت‌اند ارسال شوند.
* (من از razor page  استفاده میکنم؛ ولی مسلما در controller هم به همین شکل و راحت‌تر خواهد بود) 
7- سپس داده‌های ارسال شده‌ی توسط دیتاتیبل، به سمت سرور را با استفاده از متد زیر دریافت میکنیم:
 public static void GetDataFromRequest(this HttpRequest Request, out FiltersFromRequestDataTable filtersFromRequest)
        {
            //TODO: Make Strings Safe String
            filtersFromRequest = new();

            filtersFromRequest.draw = Request.Form["draw"].FirstOrDefault();
            filtersFromRequest.start = Request.Form["start"].FirstOrDefault();
            filtersFromRequest.length = Request.Form["length"].FirstOrDefault();
            filtersFromRequest.sortColumn = Request.Form["columns[" + Request.Form["order[0][column]"].FirstOrDefault() + "][name]"].FirstOrDefault();
            filtersFromRequest.sortColumnDirection = Request.Form["order[0][dir]"].FirstOrDefault();
            filtersFromRequest.searchValue = Request.Form["search[value]"].FirstOrDefault();
            filtersFromRequest.pageSize = filtersFromRequest.length != null ? Convert.ToInt32(filtersFromRequest.length) : 0;
            filtersFromRequest.skip = filtersFromRequest.start != null ? Convert.ToInt32(filtersFromRequest.start) : 0;
            filtersFromRequest.sortColumnIndex = Request.Form["order[0][column]"].FirstOrDefault();

            filtersFromRequest.searchValue = filtersFromRequest.searchValue?.ToLower();
        }
8- نحوه‌ی استفاده از این متد در handler یا action مورد نظر:
Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);

9- با استفاده از متد زیر، مقادیر مورد نیاز دیتاتیبل را به آن ارسال می‌کنیم:
 public static PaginationDataTableResult<T> ToDataTableJs<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
        {
            int recordsTotal = source.Count();
            CofingPaging(ref filtersFromRequest, recordsTotal);
            var result = new PaginationDataTableResult<T>()
            {
                draw = filtersFromRequest.draw,
                recordsFiltered = recordsTotal,
                recordsTotal = recordsTotal,
                data = source.OrderByIndex(filtersFromRequest).Skip(filtersFromRequest.skip).Take(filtersFromRequest.pageSize).ToList()
            };

            return result;
        }

        private static void CofingPaging(ref FiltersFromRequestDataTable filtersFromRequest, int recordsTotal)
        {
            if (filtersFromRequest.pageSize == -1)
            {
                filtersFromRequest.pageSize = recordsTotal;
                filtersFromRequest.skip = 0;
            }
        }
private static IEnumerable<T> OrderByIndex<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
        {
            var props = typeof(T).GetProperties();
            string propertyName = "";
            for (int i = 0; i < props.Length; i++)
            {
                if (i.ToString() == filtersFromRequest.sortColumnIndex)
                    propertyName = props[i].Name;
            }

            System.Reflection.PropertyInfo propByName = typeof(T).GetProperty(propertyName);
            if (propByName is not null)
            {
                if (filtersFromRequest.sortColumnDirection == "desc")
                    source = source.OrderByDescending(x => propByName.GetValue(x, null));
                else
                    source = source.OrderBy(x => propByName.GetValue(x, null));
            }

            return source;
        }
که باهر دیتاتایپی کار می‌کند و خودش به صورت خودکار، عملیات مرتب‌سازی را انجام می‌دهد (ابدایی خودم)
10- نحوه استفاده در هندلر یا اکشن:
var result =  query.ToDataTableJs(filtersFromRequest);
return new JsonResult(result);
یک نکته: پراپرتی‌های شما باید باحروف کوچک باشد وگرنه در سمت جاوااسکریپت، خطای undefined را مشاهده خواهید کرد. در این حالت باید پراپرتی‌ها را با حروف کوچک شروع کنید؛ ولی اگر دارید با کتابخانه‌ی newtonSoft  و jsonCovert سریالایز میکنید، میتوانید از این attribute بالای پراپرتی‌ها استفاده کنید: [JsonProperty("name")]
درکل باید یک iqueryrable را آماده و به متد ToDataTableJs ارسال کنید.

- برای سرچ هم در column‌ها هم  میتوانید به شکل زیر عمل کنید.
ابتدا دو متد زیر را به یک کلاس static اضافه کنید:
 public static IEnumerable<TSource> WhereSearchValue<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
        {
            return source.Where(predicate);
        }
        public static bool ContainsSearchValue(this string source, string toCheck)
        {
            return source != null && toCheck != null && source.IndexOf(toCheck, StringComparison.OrdinalIgnoreCase) >= 0;
        }
بعد به این شکل از آن‌ها بین ایجاد iqueryrable  و جایی که متد todatatableJs فراخوانی می‌شود، استفاده کنید:
if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
برای افزایش کارآیی بهتر است مدل اصلی را به ویوو ارسال نکنید و از همان اول یک IQueryrable از جنس ویوومدل یا dto داشته باشید و این سرچ را هم بر روی همان انجام دهید.

کد کامل هندلر یا action  (که ترکیب کد‌های بالا هستش):
 public JsonResult OnPostList()
        {
            Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);
            var query = _Repo.GetQueryable().Select(x => new VmAdminList()
            {
                title = x.Title,
            }
            );

            if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
                query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();

            var result =  query.ToDataTableJs(filtersFromRequest);
            return new JsonResult(result);
        }

چند نکته:
1- ممکن‌است که بخواهید یکسری فیلتر را بجز مقادیر پیش فرض به سمت سرور ارسال کنید. برای اینکار کد زیر را به قسمت Ajax  فرانت‌اند اضافه کنید:
 data: function (d) {
                    d.parentId = parentID;
                    d.StartDateTime= StartDateTime;
                },
 و آن‌ها را به این شکل در سمت سرور دریافت کنید:
if (!int.TryParse(Request.Form["parentId"].FirstOrDefault(), out int parentId))
                throw new NullReferenceException();
2- در کانفیگ‌های Ajax مربوط به دیتاتیبل، دیگر کلید Success را نداریم؛ ولی به این شکل میتوانید این قسمت را شبیه سازی کنید:
 dataSrc: function (json) {
                    $("#count").val(json.data.length);
                    var sum = 0;
                    json.data.forEach(function (item) {
                        if (!isNullOrEmpty(item.credit))
                            sum += parseInt(item.credit);
                    })
                    $("#sum").val(separate(sum));

                    return json.data;
                }
که return آن الزامی هست؛ وگرنه به خطا میخورید.

تا به اینجا کار کاملا تمام شده؛ ولی من برای داینامیک کردن schema و column‌ها هم کلاسی را نوشته‌ام که فکر میکنم کار را راحت‌تر کند. چون شما برای تعداد ستون‌ها باید یک آبجکت را به شکل زیر تعریف کنید:
columns: [
        { data: 'name' },
        { data: 'position' },
        { data: 'salary' },
        { data: 'office' }
    ]
در اینجا اگر کلید‌ها و یا ستون‌ها (<th>) جابجا باشند، خطا می‌دهد و توسعه را بعدا سخت می‌کند؛ چون بعد هر بار تغییر، باید دستی این آبجکت‌ها و ستون‌ها را هم جابجا کنید. ولی با استفاده از کد‌های زیر، خودش به صورت داینامیک تولید می‌شود. کدزیر این کار رو انجام می‌دهد:
public class JsDataTblGeneretaor<T>
    {
        public readonly DataTableSchemaResult DataTableSchemaResult = new();
        public JsDataTblGeneretaor<T> CreateTableSchema()
        {
            var props = typeof(T).GetProperties();

            foreach (var prop in props)
            {
                DataTableSchemaResult.SchemaResult.Add(new()
                {
                    data = prop.Name,
                    sortable = (prop.PropertyType == typeof(int)) || (prop.PropertyType == typeof(bool)) || (prop.PropertyType == typeof(DateTime)),
                    width = "",
                    visible = (prop.PropertyType != typeof(DateTime))
                });
            }

            return this;
        }

        public JsDataTblGeneretaor<T> CreateTableColumns()
        {
            var props = typeof(T).GetProperties();

            CustomAttributeData displayAttribute;

            foreach (var prop in props)
            {
                string displayName = prop.Name;
                displayAttribute = prop.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "DisplayAttribute");
                if (displayAttribute != null)
                {
                    displayName = displayAttribute.NamedArguments.FirstOrDefault().TypedValue.Value.ToString();
                }

                DataTableSchemaResult.Colums.Add(displayName);
            }

            return this;
        }

        public JsDataTblGeneretaor<T> AddCustomSchema(string data, bool? sortable = null, bool? visible = null, string width = null, string className = null)
        {
            if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
                return this;

            foreach (var item in DataTableSchemaResult.SchemaResult.Where(x => x.data == data))
            {
                if (sortable != null)
                    item.sortable = sortable.Value;

                if (visible != null)
                    item.visible = visible.Value;

                if (width != null)
                    item.width = width;

                if (className != null)
                    item.className = className;
            }

            return this;
        }
        public JsDataTblGeneretaor<T> SerializeSchema()
        {
            if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
                return this;

            DataTableSchemaResult.SerializedSchemaResult = JsonSerializer.Serialize(DataTableSchemaResult.SchemaResult);

            return this;
        }
    }
    public class DataTableSchema
    {
        public string data { get; set; }
        public bool sortable { get; set; }
        public string width { get; set; }
        public bool visible { get; set; }
        public string className { get; set; }
    }
    public class DataTableSchemaResult
    {
        public readonly List<DataTableSchema> SchemaResult = new();
        public readonly List<string> Colums = new();
        public string SerializedSchemaResult = "";
    }
متد CreateTableSchema، آبجکت هایی را که دیتاتیبل نیاز دارد، ایجاد می‌کند (فرقی ندارد مدلت شما از چه جنسی باشد) که شامل یک لیست از آبجکت‌هاست و شما میتوانید بااستفاده از متد AddCustomSchema، آن را سفارشی سازی کنید؛ مثلا بگوئید فلان کلید نمایش داده نشود و یا عرضش را مشخص کنید و ...  
متد CreateTableColumns  خیلی ساده هست و فقط یک لیست از استرینگ‌ها را برمیگرداند.
SerializeSchema هم که لیست آبجکت‌های مورد نیاز دیتاتیبل را سریالایز می‌کند.

نحوه استفاده:
در متد آغازین برنامه باید این کلاس را صدا بزنید و با هر روشی که دوست دارید، به view یا razor page ارسال کنید:
public void OnGet()
        {
            //Create Data Table Js Schema and Columns Dynamicly
            JsDataTblGeneretaor<yourVM> tblGeneretaor = new();
            DataTableSchemaResult = tblGeneretaor.CreateTableColumns().CreateTableSchema().SerializeSchema().DataTableSchemaResult;
        }

نحوه سفارشی سازی:
.AddCustomSchema("yourProperty",visible:false)
که به سمت View ارسال می‌شودو حالا نحوه‌ی استفاده کردن از scheme ساخته شده:
var scheme = JSON.parse('@Html.Raw(Model.DataTableSchemaResult.SerializedSchemaResult)')
و استفاده‌ی از آن در گزینه‌های دیتاتیبل  columns: scheme,

نحوه ساخت ستون‌ها در view:
@foreach (var col in Model.DataTableSchemaResult.Colums)
              {
                <th>@col</th>
              }

بازخوردهای دوره
بایدها و نبایدهای استفاده از IoC Containers
برای مواردی که در پروژه پیش میاد و مجبور هستیم تا یک نمونه کلاس بدون پارامتر رو ایجاد کنیم چه راهکاری رو پیشنهاد می‌کنید؟ چون عملاً اون کلاس نمی‌تونه وابستگی‌های خودش رو از طریق تابع سازنده اش اعلام کنه.
به عنوان مثال در کد زیر من می‌خوام یک Custom Route Constraint تعریف کنم:
routes.MapRoute(
   name: "PagesById",
   url: "Page/{id}",
   defaults: new { controller = "Route", action = "PageById", id = UrlParameter.Optional },
   constraints: new { id = new CustomPageByIdRoute() }
);
و اینجا نمی‌تونم پارامتری به کلاس CustomPageByIdRoute بدم . کلاس CustomPageByIdRoute به این صورت هست:
    public class CustomPageByIdRoute : IRouteConstraint
    {
        private readonly IUnitOfWork _uow;
        private readonly IPage _page;

        public CustomPageByIdRoute(IUnitOfWork uow, IPage page)
        {
            _uow = uow;
            _page = page;
        }

        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            return _page.FindBy(x => x.Id == Convert.ToInt32(values[parameterName]) && x.DeletedBy == 0).FirstOrDefault() != null;
        }
    }