یکی از مواردی که فشاری بر روی garbage collector را بالا میبرد، تخصیصهای حافظهی مخفی یا Hidden allocations هستند که سبب تخصیصهای حافظهی کوچک و عموما پر تعدادی بر روی heap میشوند. برای نمونه به مثال ذیل دقت کنید و سعی کنید تعداد تخصیصهای حافظهی آن را حدس بزنید:
در این مثال ... سه تخصیص حافظهی کوچک رخ میدهد. از این جهت که متد Console.WriteLine ایی که در اینجا استفاده میشود، در نهایت به یک چنین کدی کامپایل خواهد شد:
در این مثال بر روی تمام پارامترهای int دریافتی، عملیات boxing (تبدیل یا cast) به object صورت میگیرد و عملیات boxing، یک نوع allocation است که نتیجهی آن بر روی heap ذخیره میگردد.
روشی برای نمایان ساختن تخصیصهای حافظهی نهان در ویژوال استودیو
اگر از ReSharper استفاده میکنید، افزونهی «Heap Allocations Viewer» آن و یا اگر از VS 2015 و Roslyn استفاده کنید، افزونهی «Roslyn Clr Heap Allocation Analyzer» آن، سبب نمایان شدن allocationهای مخفی میشوند. برای مثال قطعه کد فوق یک چنین نمایشی را پیدا میکند:
در اینجا در ذیل هر سه موردی که عملیات boxing allocation رخ داده، یک خط قرمز کشیده است. یکی از روشهایی که میتواند boxing allocation فوق را حذف کند، بکار گیری متد ToString بر روی مقادیر int است:
همانطور که مشاهده میکنید، اینبار دیگر خبری از خطوط قرمز، ذیل پارامترهای متد Console.WriteLine نیست. باید دقت داشت که ToString نیز سبب تخصیص حافظه میشود، اما اینبار دیگر int32 آن بر روی heap ذخیره نمیگردد. به عبارتی هر دو حالت سبب تخصیص حافظهی یک رشتهی جدید میشوند؛ اما در حالت اول علاوه بر این شیء جدید، شیء int32 نیز بر روی heap ذخیره میگردد.
تشخیص تخصیص اشیاء مخفی با افزونههای Heap Allocations Viewer
نمونهی دیگر پر کاربرد این نوع بهینه سازیها را در مثال ذیل میتوان مشاهده کرد:
این مثال، یک چنین نمایش بصری دارد:
اینبار یک خط زرد رنگ ظاهر شده به همراه یک خط قرمز رنگ. خط قرمز رنگ را پیشتر بررسی کردیم و علت وجودی آن Boxing allocation ایی است که رخ میدهد. خط زرد رنگ در ذیل + ظاهر شدهاست و عنوان میکند که عملیات جمع زدن رشتهها، سبب تخصیص حافظهی یک شیء جدید میشود. رشتهها در دات نت immutable هستند. به همین جهت هر تغییری در آنها، سبب تخصیص یک شیء جدید میشود. بنابراین در همین مثال ساده، دو تخصیص حافظهی مخفی وجود دارند. مورد جمع زدن را با بکارگیری string.Format و مشکل boxing را با ToString میتوان برطرف کرد:
منابع دیگری که سبب تخصیصهای حافظهی مخفی میشوند
تا اینجا دو مورد از منابع متداول تخصیصهای حافظهی مخفی را بررسی کردیم. اما این لیست شامل موارد ذیل نیز میشود:
1) فراخوانی متدهایی با پارامترهایی از نوع param همیشه سبب تخصیص حافظهای جهت تشکیل یک آرایهی در برگیرندهی پارامترهای ارسالی میشود.
2) متدهایی که پارامتر از نوع IEnumerable دارند:
در این مثال هربار که متد Sum فراخوانی شود، یکبار دیگر IEnumerable آن تخصیص خواهد یافت که در تصویر ذیل با enumerator allocation مشخص شدهاست:
برای حل این مشکل فقط کافی است IEnumerable را با List تعویض کنید.
3) کار با LINQ نیز سبب تخصیصهای حافظهی قابل توجهی است. برای مثال در کد پایهی Roslyn، برای رسیدن به حداکثر کارآیی، بسیاری از الگوریتمها را با روشهای غیر LINQ پیاده سازی کردهاند. البته برای تیمی مانند Roslyn رسیدن به یک چنین کارآیی جهت رقابت با سایر محصولات مشابه ضروری بودهاست و گرنه در بسیاری از کارهای متداول، استفاده از LINQ به خوانایی هر چه بیشتر کدها کمک شایانی میکند.
برای مطالعهی بیشتر
Roslyn code base – performance lessons - part 2
Unusual Ways of Boosting Up App Performance. Boxing and Collections
On performance in .NET
public static void PrintSum(int a, int b) { Console.WriteLine("Sum of a {0} b {1} is {2}", a, b, a + b); }
Console::WriteLine(string, object, object, object)
روشی برای نمایان ساختن تخصیصهای حافظهی نهان در ویژوال استودیو
اگر از ReSharper استفاده میکنید، افزونهی «Heap Allocations Viewer» آن و یا اگر از VS 2015 و Roslyn استفاده کنید، افزونهی «Roslyn Clr Heap Allocation Analyzer» آن، سبب نمایان شدن allocationهای مخفی میشوند. برای مثال قطعه کد فوق یک چنین نمایشی را پیدا میکند:
در اینجا در ذیل هر سه موردی که عملیات boxing allocation رخ داده، یک خط قرمز کشیده است. یکی از روشهایی که میتواند boxing allocation فوق را حذف کند، بکار گیری متد ToString بر روی مقادیر int است:
همانطور که مشاهده میکنید، اینبار دیگر خبری از خطوط قرمز، ذیل پارامترهای متد Console.WriteLine نیست. باید دقت داشت که ToString نیز سبب تخصیص حافظه میشود، اما اینبار دیگر int32 آن بر روی heap ذخیره نمیگردد. به عبارتی هر دو حالت سبب تخصیص حافظهی یک رشتهی جدید میشوند؛ اما در حالت اول علاوه بر این شیء جدید، شیء int32 نیز بر روی heap ذخیره میگردد.
تشخیص تخصیص اشیاء مخفی با افزونههای Heap Allocations Viewer
نمونهی دیگر پر کاربرد این نوع بهینه سازیها را در مثال ذیل میتوان مشاهده کرد:
public static void PrintA(int a) { Console.WriteLine("a is " + a); }
اینبار یک خط زرد رنگ ظاهر شده به همراه یک خط قرمز رنگ. خط قرمز رنگ را پیشتر بررسی کردیم و علت وجودی آن Boxing allocation ایی است که رخ میدهد. خط زرد رنگ در ذیل + ظاهر شدهاست و عنوان میکند که عملیات جمع زدن رشتهها، سبب تخصیص حافظهی یک شیء جدید میشود. رشتهها در دات نت immutable هستند. به همین جهت هر تغییری در آنها، سبب تخصیص یک شیء جدید میشود. بنابراین در همین مثال ساده، دو تخصیص حافظهی مخفی وجود دارند. مورد جمع زدن را با بکارگیری string.Format و مشکل boxing را با ToString میتوان برطرف کرد:
public static void PrintA(int a) { Console.WriteLine("a is {0}", a.ToString()); }
منابع دیگری که سبب تخصیصهای حافظهی مخفی میشوند
تا اینجا دو مورد از منابع متداول تخصیصهای حافظهی مخفی را بررسی کردیم. اما این لیست شامل موارد ذیل نیز میشود:
1) فراخوانی متدهایی با پارامترهایی از نوع param همیشه سبب تخصیص حافظهای جهت تشکیل یک آرایهی در برگیرندهی پارامترهای ارسالی میشود.
2) متدهایی که پارامتر از نوع IEnumerable دارند:
public static int Sum(IEnumerable<int> list) { var sum = 0; foreach (var number in list) { sum += number; } return sum; }
برای حل این مشکل فقط کافی است IEnumerable را با List تعویض کنید.
3) کار با LINQ نیز سبب تخصیصهای حافظهی قابل توجهی است. برای مثال در کد پایهی Roslyn، برای رسیدن به حداکثر کارآیی، بسیاری از الگوریتمها را با روشهای غیر LINQ پیاده سازی کردهاند. البته برای تیمی مانند Roslyn رسیدن به یک چنین کارآیی جهت رقابت با سایر محصولات مشابه ضروری بودهاست و گرنه در بسیاری از کارهای متداول، استفاده از LINQ به خوانایی هر چه بیشتر کدها کمک شایانی میکند.
برای مطالعهی بیشتر
Roslyn code base – performance lessons - part 2
Unusual Ways of Boosting Up App Performance. Boxing and Collections
On performance in .NET
نوعهای Generics در دات نت از اعمال ریاضی مانند جمع و ضرب و منها پشتیبانی نمیکنند. در مقالات ذیل راه حلهایی برای رفع این مشکل ارائه شدهاند که میتوانند تبدیل به یک مقاله جدید گردند:
نظرات اشتراکها
معرفی سایت NET Fiddle.
یک سری سایت معروف هستند مثل JS Fiddle یا SQL Fiddle که به شما امکان میدن برای مطالب ارسالی در وب خودتون، مثال عینی قابل اجرا هم ارائه بدید (مطالعه کننده مطلب میتونه بلافاصله خروجی واقعی رو هم مشاهده کنه). حالا این dot net fiddle معرفی شده، برای برنامههای دات نت چنین امکانی رو فراهم کرده.
این پروژه ارتباطی به ASP.NET Core ندارد و مربوط هست به ASP.NET MVC 5. اخیرا یک سری تغییرنام صورت گرفته. ASP.NET 5 و ASP.NET MVC 6 الان هر دو به نام ASP.NET Core 1.0 معرفی شدند. EF 7 هم به نام EF Core 1.0 معرفی شدهاست. پایه اینها NET Core. است که با دات نت اصلی (و متداول) یکی نیست.
نظرات مطالب
راه اندازی وب سایت سریع و سبک با Nancy
همانطور که جناب نصیری گفتند، به این روش که آدرسها را کاربر پسند میکنیم اصطلاحا Url rewriting میگویند. این نوع آدرس دهی باعت خوانا شدن، و همچنین بهتر ایندکس شدن در موتورهای جستجو میشود.
در دات نت پروژه MVC آدرسها را طبق این اصول پیاده میکند و کاربر میتواند قاعده آدرس دهی خود را به صورت یک Route تعریف کند.
در Asp .net سنتی هم کتابخانههای Url Rewriting وجود دارند که برای شما این کار را انجام میدهند. ضمن اینکه خود شما هم با نوشتن HttpModule میتوانید این کار را انجام دهید.
در دات نت پروژه MVC آدرسها را طبق این اصول پیاده میکند و کاربر میتواند قاعده آدرس دهی خود را به صورت یک Route تعریف کند.
در Asp .net سنتی هم کتابخانههای Url Rewriting وجود دارند که برای شما این کار را انجام میدهند. ضمن اینکه خود شما هم با نوشتن HttpModule میتوانید این کار را انجام دهید.
پروژهها
DNTViewer
برنامه سورس باز DNTViewer، مشاهدهگر آفلاین اطلاعات فایل دیتابیس سایت جاری است که با فرمت XML از آدرس ذیل قابل دریافت میباشد:
برای استفاده از آن نیاز به نصب دات نت فریم ورک 4 بر روی سیستم شما است. همچنین فایل XML بانک اطلاعاتی سایت را پس از دریافت نیاز است در کنار فایل EXE برنامه کپی کنید.
برای استفاده از آن نیاز به نصب دات نت فریم ورک 4 بر روی سیستم شما است. همچنین فایل XML بانک اطلاعاتی سایت را پس از دریافت نیاز است در کنار فایل EXE برنامه کپی کنید.
کامپایلر سیشارپ اگر نتواند نوعهای عملوندها را در حین بکارگیری عملگرها تشخیص دهد، اجازهی استفاده از عملگر را نخواهد داد و کار کامپایل، با یک خطا خاتمه مییابد. برای نمونه مثال زیر را در نظر بگیرید:
در اینجا چون کامپایلر نمیداند که عملگر + بر روی چه نوعهایی قرار است اعمال شود (به علت جنریک تعریف شدن این نوعها و مشخص نبودن اینکه آیا این نوع، اصلا عملگر + دارد یا خیر)، با صدور خطای زیر، عملیات کامپایل را متوقف میکند:
برای حل این مساله، چندین روش مطرح شدهاست که در ادامه تعدادی از آنها را مرور خواهیم کرد.
روش اول: واگذار کردن استراتژی عملیات ریاضی به یک کلاس خارجی
این راه حلی است که توسط اعضای تیم سیشارپ در روزهای ابتدایی معرفی جنریکها مطرح شدهاست. فرض کنید میخواهیم لیستی از جنریکها را با هم جمع بزنیم:
این کد نیز قابل کامپایل نبوده و امکان اعمال عملگر + بر روی نوع ناشناختهی T میسر نیست.
در راه حل ارائه شده، یک اینترفیس عمومی که متد جمع را تعریف کردهاست، مشاهده میکنیم. سپس این اینترفیس در سازندهی کتابخانهی الگوریتمهای برنامه تزریق شدهاست. اکنون کدهای AlgorithmLibrary بدون مشکل کامپایل میشوند. هر زمان که نیاز به استفاده از آن بود، بر اساس نوع T، پیاده سازی خاصی را باید ارائه داد. برای مثال در اینجا Int32Calculator پیاده سازی نوع int را انجام دادهاست. برای استفاده از آن نیز خواهیم داشت:
البته این نوع پیاده سازی را که کار اصلی آن واگذاری عملیات جمع، به یک کلاس خارجی است، توسط Func نیز میتوان خلاصهتر کرد:
استفاده از Action و Func نیز یکی دیگر از روشهای تزریق وابستگیها است که در اینجا بکار گرفته شدهاست. برای استفاده از آن خواهیم داشت:
آرگومان اول روش جمع زدن را مشخص میکند و آرگومان دوم، لیستی است که باید اعضای آن جمع زده شوند.
روش دوم: استفاده از واژهی کلیدی dynamic
با استفاده از واژهی کلیدی dynamic میتوان بررسی نوع دادهها را به زمان اجرا موکول کرد. به این ترتیب دیگر کامپایلر مشکلی با کامپایل قطعه کد ذیل نخواهد داشت:
و مثال زیر نیز به خوبی کار میکند:
البته بدیهی است که نوع تعریف شده در اینجا باید دارای عملگر + باشد. در غیر اینصورت در زمان اجرا برنامه با یک خطا خاتمه خواهد یافت.
روش فوق نسبت به حالتی که بر اساس نوع T تصمیمگیری شود و از عملگر + متناظری استفاده گردد، خوانایی بهتری دارد:
روش سوم: استفاده از Expression Trees
روش زیر بسیار شبیه است به حالتیکه از Func در روش اول استفاده شد. در اینجا این Func به صورت پویا تولید و سپس صدا زده میشود:
البته این مثال، یک مثال ابتدایی در این مورد است. بر همین مبنا و ایده، یک کتابخانهی با کارآیی بالا، تحت عنوان Generic Operators که جزو Misc utils میباشد، تهیه شدهاست.
به کمک کتابخانهی Generic Operators، کدهای جمع زدن اعضای یک لیست جنریک به صورت ذیل خلاصه میشوند:
public interface ICalculator<T> { T Add(T operand1, T operand2); } public class Calculator<T> : ICalculator<T> { public T Add(T operand1, T operand2) { return operand1 + operand2; } }
Operator '+' cannot be applied to operands of type 'T' and 'T'
روش اول: واگذار کردن استراتژی عملیات ریاضی به یک کلاس خارجی
این راه حلی است که توسط اعضای تیم سیشارپ در روزهای ابتدایی معرفی جنریکها مطرح شدهاست. فرض کنید میخواهیم لیستی از جنریکها را با هم جمع بزنیم:
public class Calculator2<T> { public T Sum(List<T> list) { T sum = 0; for (int i = 0; i < list.Count; i++) sum += list[i]; return sum; } }
public interface ICalculator<T> { T Add(T operand1, T operand2); } public class Int32Calculator : ICalculator<int> { public int Add(int operand1, int operand2) { return operand1 + operand2; } } public class AlgorithmLibrary<T> where T : new() { private readonly ICalculator<T> _calculator; public AlgorithmLibrary(ICalculator<T> calculator) { _calculator = calculator; } public T Sum(List<T> items) { var sum = new T(); for (var i = 0; i < items.Count; i++) { sum = _calculator.Add(sum, items[i]); } return sum; } }
var result = new AlgorithmLibrary<int>(new Int32Calculator()).Sum(new List<int> { 1, 2, 3 });
البته این نوع پیاده سازی را که کار اصلی آن واگذاری عملیات جمع، به یک کلاس خارجی است، توسط Func نیز میتوان خلاصهتر کرد:
public class Algorithms<T> where T : new() { public T Calculate(Func<T, T, T> add, IEnumerable<T> numbers) { var sum = new T(); foreach (var number in numbers) { sum = add(sum, number); } return sum; } }
var result = new Algorithms<int>().Calculate((a, b) => a + b, new[] { 1, 2, 3 });
روش دوم: استفاده از واژهی کلیدی dynamic
با استفاده از واژهی کلیدی dynamic میتوان بررسی نوع دادهها را به زمان اجرا موکول کرد. به این ترتیب دیگر کامپایلر مشکلی با کامپایل قطعه کد ذیل نخواهد داشت:
public class Calculator<T> : ICalculator<T> { public T Add(T operand1, T operand2) { return (dynamic)operand1 + operand2; } }
var test = new Calculator<int>().Add(1, 2);
روش فوق نسبت به حالتی که بر اساس نوع T تصمیمگیری شود و از عملگر + متناظری استفاده گردد، خوانایی بهتری دارد:
public T Add(T t1, T t2) { if (typeof(T) == typeof(double)) { var d1 = (double)t1; var d2 = (double)t2; return (T)(d1 + d2); } else if (typeof(T) == typeof(int)){ var i1 = (int)t1; var i2 = (int)t2; return (T)(i1 + i2); } else ... }
روش سوم: استفاده از Expression Trees
روش زیر بسیار شبیه است به حالتیکه از Func در روش اول استفاده شد. در اینجا این Func به صورت پویا تولید و سپس صدا زده میشود:
using System; using System.Linq.Expressions; namespace GenericsArithmetic { public class Solution3 { public T Add<T>(T a, T b) { var paramA = Expression.Parameter(typeof(T), "a"); var paramB = Expression.Parameter(typeof(T), "b"); var body = Expression.Add(paramA, paramB); var add = Expression.Lambda<Func<T, T, T>>(body, paramA, paramB).Compile(); return add(a, b); } } }
به کمک کتابخانهی Generic Operators، کدهای جمع زدن اعضای یک لیست جنریک به صورت ذیل خلاصه میشوند:
public static T Sum<T>(this IEnumerable<T> source) { T sum = Operator<T>.Zero; foreach (T value in source) { sum = Operator.Add(sum, value); } return sum; }
تسکهای پس زمینه (Background Job) چیست؟
بطور کلی تسکهای پس زمینه، کارهایی هستند که برنامه باید بصورت خودکار در زمانهای مشخصی آنها را انجام دهد؛ برای مثال شرایطی را در نظر بگیرید که متدی را با حجم زیادی از محاسبات پیچیده دارید که وقتی کاربر درخواست خود را ارسال میکند، شروع به محاسبه میشود و کاربر چارهای جز انتظار نخواهد داشت؛ اما اگر اینکار در زمانی دیگر، قبل از درخواست کاربر محاسبه میشد و صرفا نتیجهاش به کاربر نمایش داده میشد، قطعا تصمیم گیری بهتری نسبت به محاسبهی آنی آن متد، در زمان درخواست کاربر بوده.
در سناریوی دیگری تصور کنید میخواهید هر شب در ساعتی مشخص، خلاصهای از مطالب وبسایتتان را برای کاربران وبسایت ایمیل کنید. در این حالت برنامه باید هر شب در ساعتی خاص اینکار را برای ما انجام دهد و تماما باید این اتفاق بدون دخالت هیچ ارادهی انسانی و بصورت خودکار توسط برنامه انجام گیرد.
همچنین شرایطی از این قبیل، ارسال ایمیل تایید هویت، یک ساعت بعد از ثبت نام، گرفتن بک آپ از اطلاعات برنامه بصورت هفتگی و دیگر این موارد، همه در دستهی تسکهای پس زمینه (Background Job) از یک برنامه قرار دارند.
سؤال : HangFire چیست؟
همانطور که دانستید، تسکهای پس زمینه نیاز به یک سیستم مدیریت زمان دارند که کارها را در زمانهای مشخص شدهای به انجام برساند. HangFire یک پکیج متن باز، برای ایجاد سیستم زمانبندی شدهی کارها است و اینکار را به سادهترین روش، انجام خواهد داد. همچنین HangFire در کنار Quartz که سیستم دیگری جهت پیاده سازی زمانبندی است، از معروفترین پکیجها برای زمانبندی تسکهای پس زمینه بشمار میروند که در ادامه بیشتر به مزایا و معایب این دو میپردازیم.
مقایسه HangFire و Quartz
میتوان گفت این دو پکیج تا حد زیادی شبیه به هم هستند و تفاوت اصلی آنها، در لایههای زیرین و نوع محاسبات زمانی هریک، نهفته است که الگوریتم مختص به خود را برای این محاسبات دارند؛ اما در نهایت یک کار را انجام میدهند.
دیتابیس :
تفاوتی که میتوان از آن نام برد، وجود قابلیت Redis Store در HangFire است که Quratz چنین قابلیتی را از سمت خودش ارائه نداده و برای استفاده از Redis در Quartz باید شخصا این دو را باهم کانفیگ کنید. دیتابیس Redis بخاطر ساختار دیتابیسی که دارد، سرعت و پرفرمنس بالاتری را ارائه میدهد که استفاده از این قابلیت، در پروژههایی با تعداد تسکها و رکوردهای زیاد، کاملا مشهود است. البته این ویژگی در HangFire رایگان نیست و برای داشتن آن از سمت HangFire، لازم است هزینهی آن را نیز پرداخت کنید؛ اما اگر هم نمیخواهید پولی بابتش بپردازید و همچنان از آن استفاده کنید، یک پکیج سورس باز برای آن نیز طراحی شده که از این لینک میتوانید آنرا مشاهده کنید.
ساختار :
پکیج HangFire از ابتدا با دات نت و معماریهای دات نتی توسعه داده شده، اما Quartz ابتدا برای زبان جاوا نوشته شده بود و به نوعی از این زبان، ریلیزی برای دات نت تهیه شد و این موضوع طبعا تاثیرات خودش را داشته و برخی از معماریها و تفکرات جاوایی در آن مشهود است که البته مشکلی را ایجاد نمیکند و محدودیتی نسبت به HangFire از لحاظ کارکرد، دارا نیست. شاید تنها چیزی که میتوان در این باب گفت، DotNet Friendlyتر بودن HangFire است که کار با متدهای آن، آسانتر و به اصطلاح، خوش دستتر است.
داشبورد :
هردو پکیج از داشبورد، پشتیبانی میکنند که میتوانید در این داشبورد و ui اختصاصی که برای نمایش تسکها طراحی شده، تسکهای ایجاد شده را مدیریت کنید. داشبورد HangFire بصورت پیشفرض همراه با آن قرار دارد که بعد از نصب HangFire میتوانید براحتی داشبورد سوار بر آن را نیز مشاهده کنید. اما در Quartz ، داشبورد باید بصورت یک Extension، در پکیجی جدا به آن اضافه شود و مورد استفاده قرار گیرد. در لینکهای 1 و 2، دوتا از بهترین داشبوردها برای Quartz را مشاهده میکنید که در صورت نیاز میتوانید از آنها استفاده کنید.
استفاده از HangFire
1. نصب :
- برای نصب HangFire در یک پروژهی Asp.Net Core لازم است ابتدا پکیجهای مورد نیاز آن را نصب کنید؛ که شامل موارد زیر است:
Install-Package Hangfire.Core Install-Package Hangfire.SqlServer Install-Package Hangfire.AspNetCore
- پس از نصب پکیجها باید تنظیمات مورد نیاز برای پیاده سازی HangFire را در برنامه، اعمال کنیم. این تنظیمات شامل افزودن سرویسها و اینترفیسهای HangFire به برنامه است که اینکار را با افزودن HangFire به متد ConfigureService کلاس Startup انجام خواهیم داد:
public void ConfigureServices(IServiceCollection services) { // Add Hangfire services. services.AddHangfire(configuration => configuration .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions { CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.Zero, UseRecommendedIsolationLevel = true, DisableGlobalLocks = true })); // Add the processing server as IHostedService services.AddHangfireServer(); // Add framework services. services.AddMvc(); }
- پکیج HangFire برای مدیریت کار و زمان ، Table هایی دارد که پس از نصب، بر روی دیتابیس برنامهی شما قرار میگیرد. فقط باید دقت داشته باشید ConnectionString دیتابیس خود را در متد AddHangFire مقدار دهی کنید، تا از این طریق دیتابیس برنامه را شناخته و Tableهای مورد نظر را در Schema جدیدی با نام HangFire به آن اضافه کند.
پ ن : HangFire بصورت پیشفرض با دیتابیس SqlServer ارتباط برقرار میکند.
- این پکیج یک داشبورد اختصاصی دارد که در آن لیستی از انواع تسکهای در صف انجام و گزارشی از انجام شدهها را در اختیار ما قرار میدهد. برای تنظیم این داشبورد باید Middleware مربوط به آن و endpoint جدیدی را برای شناسایی مسیر داشبورد HangFire در برنامه، در متد Configure کلاس Startup اضافه کنید :
public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IHostingEnvironment env) { // HangFire Dashboard app.UseHangfireDashboard(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); // HangFire Dashboard endpoint endpoints.MapHangfireDashboard(); }); }
برای اینکه به داشبورد HangFire دسترسی داشته باشید، کافیست پس از نصب و انجام تنظیمات مذکور، برنامه را اجرا کنید و در انتهای Url برنامه، کلمهی "hangfire" را وارد کنید. سپس وارد پنل داشبورد آن خواهید شد.
http://localhost:50255/hangfire
app.UseHangfireDashboard("/mydashboard");
http://localhost:50255/mydashboard
2. داشبورد :
داشبورد HangFire شامل چندین بخش و تب مختلف است که به اختصار هر یک را بررسی خواهیم کرد.
تب Job :
همهی تسکهای تعریف شده، شامل Enqueued, Succeeded, Processing, Failed و... در این تب نشان داده میشوند.
تب Retries :
این تب مربوط به تسکهایی است که در روال زمانبندی و اجرا، به دلایل مختلفی مثل Stop شدن برنامه توسط iis یا Down شدن سرور و یا هر عامل خارجی دیگری، شکست خوردند و در زمانبندی مشخص شده، اجرا نشدند. همچنین قابلیت دوبارهی به جریان انداختن job مورد نظر را در اختیار ما قرار میدهد که از این طریق میتوان تسکهای از دست رفته را مدیریت کرد و دوباره انجام داد.
تب Recurring Jobs :
وقتی شما یک تسک را مانند گرفتن بکاپ از دیتابیس، بصورت ماهانه تعریف میکنید و قرار است در هر ماه، این اتفاق رخ دهد، این مورد یک تسک تکراری تلقی شده و این تب مسئول نشان دادن اینگونه از تسکها میباشد.
تب Servers :
این بخش، سرویسهایی را که HangFire برای محاسبهی زمانبندی از آنها استفاده میکند، نشان میدهد. وقتی متد services.AddHangfireServer را به متد ConfigureService کلاس Startup اضافه میکنید، سرویسهای HangFire جهت محاسبهی زمانبندیها فعال میشوند.
3. امنیت داشبورد :
همانطور که دانستید، داشبورد، اطلاعات کاملی از نوع کارها و زمان اجرای آنها و نام متدها را در اختیار ما قرار میدهد و همچنین اجازهی تغییراتی را مثل حذف یک تسک، یا دوباره به اجرا در آوردن تسکها و یا اجرای سریع تسکهای به موعد نرسیده را به کاربر میدهد. گاهی ممکن است این اطلاعات، شامل محتوایی امنیتی و غیر عمومی باشد که هرکسی در برنامه حق دسترسی به آنها را ندارد. برای مدیریت کردن این امر، میتوانید مراحل زیر را طی کنید :
مرحله اول : یک کلاس را ایجاد میکنیم (مثلا با نام MyAuthorizationFilter) که این کلاس از اینترفیسی با نام IDashboardAuthorizationFilter ارث بری خواهد کرد.
public class MyAuthorizationFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) { var httpContext = context.GetHttpContext(); // Allow all authenticated users to see the Dashboard (potentially dangerous). return httpContext.User.Identity.IsAuthenticated; } }
درون این کلاس، متدی با نام Authorize از اینترفیس مربوطه، پیاده سازی میشود که شروط احراز هویت و صدور یا عدم صدور دسترسی را کنترل میکند. این متد، یک خروجی Boolean دارد که اگر هر یک از شروط احراز هویت شما تایید نشد، خروجی false را بر میگرداند. در این مثال، ما برای دسترسی، محدودیت Login بودن را اعمال کردهایم که این را از HttpContext میگیریم.
مرحله دوم : در این مرحله کلاسی را که بعنوان فیلتر احراز هویت برای کاربران ساختهایم، در optionهای middleware پکیج HangFire اضافه میکنیم.
app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new [] { new MyAuthorizationFilter() } });
app.UseHangfireDashboard("/hangfire", new DashboardOptions { IsReadOnlyFunc = (DashboardContext context) => true });
این گزینه اجازهی هرگونه تغییری را در روند تسکها، از طریق صفحهی داشبورد، از هر کاربری سلب میکند و داشبورد را صرفا به جهت نمایش کارها استفاده میکند نه چیز دیگر.
انواع تسکها در HangFire :
1. تسکهای Fire-And-Forget :
تسکهای Fire-And-Forget زمانبندی خاصی ندارند و بلافاصله بعد از فراخوانی، اجرا میشوند. برای مثال شرایطی را در نظر بگیرید که میخواهید پس از ثبت نام هر کاربر در وبسایت، یک ایمیل خوش آمد گویی ارسال کنید. این عمل یک تسک پس زمینه تلقی میشود، اما زمانبندی خاصی را نیز نمیخواهید برایش در نظر بگیرید. در چنین شرایطی میتوانید از متد Enqueue استفاده کنید و یک تسک Fire-And-Forget را ایجاد کنید تا این تسک صرفا در تسکهای پس زمینهتان نام برده شود و قابل مشاهده باشد.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = backgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName) { var jobId = _backgroundJobClient.Enqueue(() => SendWelcomeMail(userName)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
2. تسکهای Delayed :
همانطور که از اسم آن پیداست، تسکهای Delayed، تسکهایی هستند که با یک تاخیر در زمان، اجرا خواهند شد. بطور کلی زمانبندی این تسکها به دو دسته تقسیم میشود :
- دسته اول : اجرا پس از تاخیر در زمانی مشخص.
همان شرایط ارسال ایمیل را به کاربرانی که در وبسایتتان ثبت نام میکنند، در نظر بگیرید؛ اما اینبار میخواهید نه بلافاصله، بلکه 10 دقیقه بعد از ثبت نام کاربر، ایمیل خوش آمد گویی را ارسال کنید. در این نوع شما یک تاخیر 10 دقیقهای میخواهید که Delayed Jobها اینکار را برای ما انجام میدهند.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = backgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName) { var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),TimeSpan.FromMinutes(10)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
همچنین میتوانید از ورودیهای دیگر نوع TimeSpan شامل TimeSpan.FromMilliseconds و TimeSpan.FromSecondsو TimeSpan.FromMinutes و TimeSpan.FromDays برای تنظیم تاخیر در تسکهای خود استفاده کنید.
- دسته دوم : اجرا در زمانی مشخص.
نوع دیگر استفاده از متد Schedule، تنظیم یک تاریخ و زمان مشخصی برای اجرا شدن تسکهای در آن تاریخ و زمان واحد میباشد. برای مثال سناریویی را در نظر بگیرید که دستور اجرا و زمانبندی آن، در اختیار کاربر باشد و کاربر بخواهد یک Reminder، در تاریخ مشخصی برایش ارسال شود که در اینصوررت میتوانید با استفاده از instance دیگری از متد Schedule که ورودی ای از جنس DateTimeOffset را دریافت میکند، تاریخ مشخصی را برای آن اجرا، انتخاب کنید.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = IBackgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName , DateTime dateAndTime) { var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),DateTimeOffset(dateAndTime)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
در این مثال، تاریخ مشخصی را برای اجرای تسکهای خود، از کاربر، در ورودی اکشن متد دریافت کردهایم و به متد Schedule، در غالب DateTimeOffset تعریف شده، پاس میدهیم.
3. تسکهای Recurring :
تسکهای Recurring به تسکهایی گفته میشود که باید در یک بازهی گردشی از زمان اجرا شوند. در یک مثال، بیشتر با آن آشنا خواهیم شد. فرض کنید میخواهید هر هفته، برنامه از اطلاعات دیتابیس موجود بکاپ بگیرد. در اینجا تسکی را دارید که قرار است هر هفته و هربار به تکرر اجرا شود.
public class HomeController : Controller { private readonly IRecurringJobManager _recurringJobManager; public HomeController(IRecurringJobManager recurringJobManager) { _recurringJobManager = recurringJobManager; } [HttpPost] [Route("BackUp")] public IActionResult BackUp(string userName) { _recurringJobManager.AddOrUpdate("test", () => BackUpDataBase(), Cron.Weekly); return Ok(); } public void BackUpDataBase() { // ... } }
برای تنظیم یک Recurring Job باید اینترفیس دیگری را بنام IRecurringJobManager، تزریق کرده و متد AddOrUpdate را استفاده کنید. در ورودی این متد، یک نوع تعریف شده در HangFire بنام Cron دریافت میشود که بازهی گردش در زمان را دریافت میکند که در اینجا بصورت هفتگی است.
انواع دیگر Cron شامل :
- هر دقیقه (Cron.Minutely) :
این Cron هر دقیقه یکبار اجرا خواهد شد.
_recurringJobManager.AddOrUpdate("test", () => job , Cron.Minutely);
- هر ساعت (Cron.Hourly) :
این Cron هر یک ساعت یکبار و بصورت پیشفرض در دقیقه اول هر ساعت اجرا میشود.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly(10));
- هر روز (Cron.Daily) :
این Cron بصورت روزانه و در حالت پیشفرض، در اولین ساعت و اولین دقیقهی هر روز اجرا خواهد شد.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily(3,10));
- هر هفته (Cron.Weekly) :
این Cron هفتگی است. بصورت پیشفرض هر هفته، شنبه در اولین ساعت و در اولین دقیقه، اجرا میشود.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Weekly);
_recurringJobManager.AddOrUpdate("test", () => Job,Cron.Weekly(DayOfWeek.Monday,3,10));
- هر ماه (Cron.Monthly) :
این Cron بصورت ماهانه اولین روز ماه در اولین ساعت روز و در اولین دقیقه ساعت، زمانبندی خود را اعمال میکند.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly(10,3,10));
- هر سال (Cron.Yearly) :
و در نهایت این Cron بصورت سالانه و در اولین ماه، روز، ساعت و دقیقه هر سال، وظیفه خود را انجام خواهد داد.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly(2,4,3,10));
در نهایت با استفاده از این Cronها میتوانید انواع مختلفی از Recurring Jobها را بسازید.
4. تسکهای Continuations :
این نوع از تسکها، وابسته به تسکهای دیگری هستند و بطور کلی وقتی استفاده میشوند که ما میخواهیم تسکی را پس از تسک دیگری، با یک زمانبندی، به نسبت زمان اجرای تسک اول، اجرا کنیم. برای مثال میخواهیم 10 دقیقه بعد از ثبت نام کاربر، برای او ایمیل احراز هویت ارسال شود که شبیه اینکار را در تسکهای Delayed انجام داده بودیم. اما همچنین قصد داریم 5 دقیقه بعد از ارسال ایمیل احراز هویت، لینک فرستاده شده را منسوخ کنیم. در این سناریو ما دو زمانبندی داریم؛ اول 10 دقیقه بعد از ثبت نام کاربر و دوم 5 دقیقه بعد از اجرای متد اول.
var stepOne = _backgroundJobClient.Schedule(() => SendAuthorizationEmail(), TimeSpan.FromMinute(10)); _backgroundJobClient.ContinueJobWith(stepOne, () => _backgroundJobClient.Schedule(() => ExpireAuthorizationEmail(), TimeSpan.FromHours(5)));
برای ایجاد یک Continuations Job باید از متد ContinueJobWith در اینترفیس IBackgroundJobClient استفاده کنیم و در ورودی اول این متد، آیدی تسک ایجاد شده قبلی را پاس دهیم.
برخی از نکات و ترفندهای HangFire
1. استفاده از Cron Expression در Recurring Jobها :
بطور کلی، Cron، ساختاری تعریف شده برای تعیین بازههای زمانی است. Cron اختصار یافتهی کلمات Command Run On میباشد که به اجرا شدن یک دستور، در زمان مشخصی اشاره دارد. برای استفاده از آن، ابتدا به تعریف این ساختار میپردازیم :
* * * * * command to be executed - - - - - | | | | | | | | | ----- Day of week (0 - 7) (Sunday=0 or 7) | | | ------- Month (1 - 12) | | --------- Day of month (1 - 31) | ----------- Hour (0 - 23) ------------- Minute (0 - 59)
* * * * *
- فیلد اول (Minute) : در این فیلد باید دقیقهای مشخص از یک ساعت را وارد کنید؛ مانند دقیقه 10 (میتوانید محدودهای را هم تعیین کنید)
- فیلد دوم (Hour) : در این فیلد باید زمان معلومی را با فرمت ساعت وارد کنید؛ مانند ساعت 7 (میتوانید محدودهای را هم تعیین کنید، مانند ساعات 12-7)
- فیلد سوم (Day of Month) : در این فیلد باید یک روز از ماه را وارد کنید؛ مانند روز 15 ام از ماه (میتوانید محدودهای را هم تعیین کنید)
- فیلد چهارم (Month) : در این فیلد باید یک ماه از سال را وارد کنید؛ مثلا ماه 4 ام(آوریل) (میتوانید محدودهای را هم تعیین کنید)
- فیلد پنجم (Day of Week) : در این فیلد باید روزی از روزهای هفته یا محدودهای از آن روزها را تعیین کنید. مانند صفرم هفته که در کشورهای اروپایی و آمریکایی معادل روز یکشنبه است.
همانطور که میبینید، Cronها دسترسی بهتری از تعیین بازههای زمانی مختلف را ارائه میدهند که میتوانید از آن در Recurring متدها بجای ورودیهای Yearly - Monthly - Weekly - Daily - Hourly - Minutely استفاده کنید. در واقع خود این ورودیها نیز متدی تعریف شده در کلاس Cron هستند که با فراخوانی آن، خروجی Cron Expression را میسازند و در درون ورودی متد Recurring قرار میگیرند.
در ادامه مثالی را خواهیم زد تا نیازمندی به Cron Expressionها را بیشتر درک کنید. فرض کنید میخواهید یک زمانبندی داشته باشید که "هر ماه بین بازه 10 ام تا 15 ام، بطور روزانه در ساعت 4:00" اجرا شود. اعمال این زمانبندی با متدهای معمول در کلاس Cron امکان پذیر نیست؛ اما میتوانید با Cron Expression آنرا اعمال کنید که به این شکل خواهد بود:
0 4 10-15 * *
برای ساخت Cron Expressionها وبسایت هایی وجود دارند که کمک میکنند انواع Cron Expressionهای پیچیدهای را طراحی کنیم و با استفاده از آن، زمانبندیهای دقیقتر و جزئیتری را بسازیم. یکی از بهترین وبسایتها برای اینکار crontab.guru است.
پ. ن. برای استفاده از Cron Expression در متدهای Recurring کافی است بجای ورودیهای Yearly - Monthly - Weekly - Daily - Hourly - Minutely ، خود Cron Expression را درون ورودی متد تعریف کنیم :
_recurringJobManager.AddOrUpdate("test", () => job , "0 4 10-15 * *" );
2. متد Trigger :
متد Trigger یک متد برای اجرای آنی تسکهای Recurring است که به کمک آن میتوانید این نوع از تسکها را بدون در نظر گرفتن زمانبندی آنها، در لحظه اجرا کنید و البته تاثیری در دفعات بعدی تکرار نداشته باشد.
RecurringJob.Trigger("some-id");
3. تعیین تاریخ انقضاء برای Recurring Jobها :
گاهی ممکن است در تسکهای Recurring شرایطی پیش آید که برفرض میخواهید کاری را هر ماه انجام دهید، اما این تکرار در پایان همان سال تمام میشود. در اینصورت باید یک Expire Time برای متد Recurring خود تنظیم کنیم تا بعد از 12 ماه تکرار، در تاریخ 140X/12/30 به پایان برسد. HangFire برای متدهای Recurring ورودی با عنوان ExpireTime تعریف نکرده، اما میتوان از طریق ایجاد یک زمانبندی، تاریخ مشخصی را برای حذف کردن متد Recurring تعریف کرد که همانند یک ExpireTime عمل میکند.
_recurringJobManager.AddOrUpdate("test", () => Console.WriteLine("Recurring Job"), Cron.Monthly); _backgroundJobClient.Schedule(() => _recurringJobManager.RemoveIfExists("test"), DateTimeOffset(dateAndTime));
با اجرای این متد، اول کاری برای تکرار در زمانبندی ماهیانه ایجاد میشود و در متد دوم، زمانی برای حذف متد اول مشخص میکند.
در آخر امیدوارم این مقاله برایتان مفید واقع شده باشد. میتوانید فیدبکتان را در قالب کامنت یا یک قهوه برایم ارسال کنید.
نظرات اشتراکها
مقایسه کارآیی ORMهای مطرح دات نت
ADO.NET شبیه به زبان اسمبلی دسترسی به اطلاعات در دات نت هست. این پایینترین سطحی است که برای کار با بانکهای اطلاعاتی در دنیای دات نت وجود دارد. تمام ORMهای موجود لایهای هستند بر روی این سطح.