بازخوردهای دوره
متدهای async تقلبی
«...در این تست متد async  هر بار زمان بیشتری برای محاسبه برد (که خوب چون ترد جدید باز میکنه طبیعی هست)...»
- زمان بیشتر بردن کاملا طبیعی هست؛ چون متد async یک Coroutines را در پشت صحنه ایجاد می‌کند (کدهای قسمتی از آن توسط کامپایلر به این صورت تولید می‌شود):
و این Coroutines نسبت به حالت sync، تعداد سیکل CPU بیشتری را مصرف می‌کنند. اما نکته‌ی اصلی این است که عملیات async ترد جدیدی هم ایجاد نمی‌کند (دلیل اصلی علاقه‌ی به آن این است و نه اینکه تعداد سیکل CPU کمتری دارد). بحث اصلی ما بالا رفتن میزان «مقیاس‌پذیری» سیستم هست و نه بالا رفتن سرعت اجرای قطعه کدی خاص که مطلقا چنین چیزی نیست.
+ متد WriteAsync برای نوشتن در FileStream (در استریم فایل سیستم) و یا نوشتن در استریم یک سوکت شبکه هست (IO-bound operations) و نه نوشتن در حافظه (MemoryStream) به صورت async که حاصلی ندارد (خلاصه‌ی بحث جاری یا انجام عملیاتی CPU-bound operation). اهمیت نوشتن async در فایل سیستم و یا استریم سوکت شبکه هم در همان مطلب «اعمال غیر همزمان و چند ریسمانی» بحث شده و همچنین سایر قسمت‌های این سری. بحث متدهای Async اضافه شده به دات نت فریم ورک، ربطی به مباحث چند ریسمانی ندارد «... متدهای Async واقعی کار با شبکه و اعمال I/O، از ترد استفاده نمی‌کنند ...». به همین جهت نسبت به حالت استفاده از تردها سربار کمتری دارند.  
بازخوردهای دوره
استفاده از Async و Await در برنامه‌های ASP.NET MVC
- پیشنیاز مطالعه قسمت جاری، مطالعه 6 قسمت اول این دوره است.
- «همزمان اجرا میشه»
خیر. متدهای Async واقعی مثل نمونه ارائه شده در EF غیرهمزمان اجرا می‌شوند. یعنی، ترد جاری را آزاد کرده و ASP.NET می‌تواند از آن ترد برای پاسخ دهی به یک درخواست رسیده دیگر استفاده کند.
- «باید منتظر پاسخ از db بمونه»
استفاده از await و async سبب بازنویسی بدنه متد توسط یک state machine در پشت صحنه می‌شوند. یعنی اینطور نیست که روش اجرای آن blocking است و تا رسیدن پاسخ از بانک اطلاعاتی، از این ترد دیگر نمی‌شود استفاده کرد. جایی که await فراخوانی می‌شود، ترد جاری برای استفاده بعدی آزاد خواهد شد. در ادامه مابقی کدها تبدیل به یک IEnumerator می‌شوند که هر دستور آن شامل یک yield return است. هر مرحله که تمام شد، MoveNext این IEnumerator فراخوانی می‌شود تا به مرحله‌ی بعدی برسد. به این روش استفاده از coroutines هم گفته می‌شود که در سی شارپ 5، کامپایلر کار تولید کدهای آن‌را انجام می‌دهد. برای مطالعه بیشتر:
انجام پی در پی اعمال Async به کمک Iterators - قسمت اول  
انجام پی در پی اعمال Async به کمک Iterators - قسمت دوم
- «چون تا فایل آپلود نشه ذخیره آدرس تو db بی معنیه»
ذخیره آدرس هم یک قسمت از کار است و اتفاقا وابسته به سیستم جاری هم نیست. وابسته است به یک بانک اطلاعاتی که خارج از مرزهای سیستم، به صورت مستقل در حال فعالیت است (عموما البته؛ مثلا اگر از SQL Server استفاده می‌شود).
برای ذخیره فایل‌ها در سیستم هم متدهای Async به کلاس Stream در دات نت 4.5 اضافه شده‌اند؛ مثل WriteAsync . در این حالت هم می‌توان از await WriteAsync برای ذخیره اطلاعات و بازهم آزاد کردن ترد جاری استفاده کرد.
نظرات مطالب
خلاصه‌ای کوتاه در مورد WinRT
- بحث وب که سرجای خودش همانند سابق هست و هیچ فرقی نمی‌کند. برنامه‌های ASP.NET روی سرور اجرا می‌شوند و عموما روی سرور بجز یک سری سرویس‌های ویندوز NT‌ ، هیچ نرم افزار دیگری نصب نخواهد شد. مثلا IIS یا مثلا SQL Server و در همین حد. حتی عموما سرورها حتی مونیتور هم ندارند و با ریموت دسکتاپ یک سری کارهای مدیریتی آن‌ها را انجام می‌دهند و این کارها هم طوری نیست که هر روز تغییر کند. یکبار سرور تنظیم می‌شود که حداقل یکسال یا بیشتر کار کند. این مورد اصلا تغییری نخواهد داشت. بحث سمت سرور است. بنابراین سرمایه گذاری روی ASP.NET خوب است و شامل این بحث ویندوز 8 یا ویندوزهای بعدی نمی‌شود؛ چون این‌ها (WinRT) سمت کاربر محسوب می‌شوند.
- از این جهت که رابط‌های کاربری مبتنی بر WinRT ، یا بر پایه XAML است یا HTML/CSS ، یادگیری WPF و یا سیلورلایت (که قسمتی از WPF را به ارث برده) مفید خواهند بود؛ از این لحاظ که پایه رابط کاربری هر دوی این‌ها هم XAML است و اساسا طراحی XAML از اینجا به WinRT منتقل شده.

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

- یادگیری سی++ همیشه مفید است. حتی در کره مریخ هم تاجایی که اطلاع دارم (!) یک کامپایلر سی++ وجود دارد و می‌شود با آن برنامه‌ی Hello world را کامپایل کرد. اگر باور ندارید از این لینوکسی‌ها بپرسید!
نظرات مطالب
ASP.NET MVC #16
- با debug=false هم انجام میشه. برنامه رو خارج از ویژوال استودیو اجرا کنید. بحث دیباگ کد در VS.NET با مدیریت خطا توسط ASP.NET متفاوت است. debug=false برنامه ASP.NET رو تبدیل به حالت Release می‌کنه اما به این معنا نیست که مکانیزم‌های ASP.NET رو کلا از کار می‌اندازه. مصرف حافظه برنامه کمتر میشه. debug symbols از اسمبلی نهایی حذف خواهند شد. کامپایلر روی کد نهایی بهینه سازی‌هایی در جهت بالابردن سرعت انجام می‌ده.
- در MVC اگر از فیلتر Handle Error استفاده بشه application_error فراخوانی نخواهد شد. به علاوه با وجود ELMAH ضرورتی به استفاده از Handle error و application_error نیست. توضیح دادم در متن فوق. 
ضمن اینکه دیباگ کار شخصی افراد نیاز به بودن پروژه آن‌ها و بررسی آن(ها در یک انجمن مرتبط یا توسط یک مشاور) دارد. این جملات مبهم و کلی «من یه کاری یه جایی کردم، یکی یه چیزی گفت، برای من کار نکرد» ارزش یا امکان بررسی ندارند.
-
custom errors قرار گرفته در ریشه سایت بر روی کل سایت اعمال می‌شود (و تمام برنامه‌هایی که در زیر پوشه‌های آن هستند). در فایل‌های کانفیگ ASP.NET مباحث ارث بری وجود دارند. برای لغو آن یکی از راه‌ها استفاده از تگ location است، مثلا:
<location path="MyAreaName">
  <system.web>
    <customErrors mode="On" defaultRedirect="/MyAreaName/error" />
  </system.web>    
</location>

مطالب
آموزش زمانبندی کارها با HangFire در Asp.Net Core

تسک‌های پس زمینه (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
البته میتوانید آدرس داشبورد HangFire را در برنامه، از کلمه‌ی "hangfire" به هر چیزی که میخواهید شخصی سازی کنید. برای اینکار کافیست درون Middleware تعریف شده بصورت ورودی string، آدرس جدیدی را برای HangFire تعریف کنید. 
app.UseHangfireDashboard("/mydashboard");
و به طبع در Url : 
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() }
});
یکی دیگر از option‌های این middleware که میتوان برای کنترل دسترسی در HangFire استفاده کرد، گزینه‌ی Read-only view نام دارد. 
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}");
         }    
    }
همانطور که میبینید در مثال بالا ابتدا برای استفاده از تسک‌های Fire-and-Forget در HangFire باید اینترفیس IBackgroundJobClient را تزریق کنیم و با استفاده از متد Enqueue در این اینترفیس، یک تسک پس زمینه را ایجاد میکنیم که کار آن، فراخوانی متد SendWelcomeMail خواهد بود.

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}");
         }    
    }
در این مثال با استفاده از متد Schedule در اینترفیس IBackgroundJobClient توانستیم متد SendWelcomeMail را صدا بزنیم و با ورودی TimeSpan یک تاخیر 10 دقیقه‌ای را در متد HangFire اعمال کنیم.
همچنین میتوانید از ورودی‌های دیگر نوع 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);
اما میتوانید یک ورودی دقیقه به آن بدهید که در اینصورت در N اُمین دقیقه از هر ساعت اجرا شود. 
 _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));
با اجرای این متد، اول کاری برای تکرار در زمانبندی ماهیانه ایجاد میشود و در متد دوم، زمانی برای حذف متد اول مشخص میکند.

در آخر امیدوارم این مقاله برایتان مفید واقع شده باشد. میتوانید فیدبکتان را در قالب کامنت یا یک قهوه برایم ارسال کنید.

بازخوردهای دوره
نگاهی به SignalR Hubs
یک hub را داخل یک پروژه دیگر از نوع class library  قرار دادم و با ارجاع به یک کنسول که selfhost شده، میخواستم ازش استفاده کنم، کار نمیکند. به همین سادگی!
کد hub
[HubName("messageHub")]
    public class MessageHub : Hub
    {
        public void NotifyAllClients()
        {
            Clients.All.Notify();
        } 
    }
کلاس startup
public partial class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            var hubConfiguration = new HubConfiguration()
            {
                EnableDetailedErrors = true
            };

            appBuilder.MapSignalR(hubConfiguration);

            appBuilder.UseCors(CorsOptions.AllowAll);


        }
    }
نقطه آغازین برنامه:
static void Main(string[] args)
        {
            const string baseAddress = "http://localhost:9000/"; // "http://*:9000/";

            using (var webapp = WebApp.Start<Startup>(baseAddress))
            {
                Console.WriteLine("Start app...");

                var hubConnection = new HubConnection(baseAddress);
                IHubProxy messageHubProxy = hubConnection.CreateHubProxy("messageHub");

                messageHubProxy.On("notify", () =>
                {
                    Console.WriteLine();
                    Console.WriteLine("Notified!");
                });

                hubConnection.Start().Wait();

                Console.WriteLine("Start signalr...");

                bool dontExit = true;
                while (dontExit)
                {
                    var key = Console.ReadKey();
                    if (key.Key == ConsoleKey.Escape) dontExit = false;

                    messageHubProxy.Invoke("NotifyAllClients");
                }

            }
        }
اگر کلاس hub را به داخل پروژه‌ی slefhost منتقل کنم، کار میکند. اما در یک class library دیگر خیر.
نگارش دات نت و ارجاعات همه یکسان است. dotnet 4.5 
آیا نکته ای جاانداخته شده در این نمونه کد؟
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS
یک نکته‌ی تکمیلی: ساده‌تر شدن به روز رسانی برنامه‌ها در ASP.NET Core 6x
تا پیش از دات نت 6، فایل‌های باینری برنامه‌های ASP.NET Core توسط IIS قفل می‌شوند و امکان جایگزینی آن‌ها بدون متوقف کردن برنامه، میسر نیست. برای رفع این مشکل، پشتیبانی از shadow copy به ASP.NET Core Module for IIS جدید، اضافه شده که روش فعالسازی آن با انجام تغییراتی در Web.Config به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!-- To customize the asp.net core module uncomment and edit the following section. 
  For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->

  <system.webServer>
    <handlers>
      <remove name="aspNetCore"/>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModulev2" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout">
      <handlerSettings>
        <handlerSetting name="experimentalEnableShadowCopy" value="true" />
        <handlerSetting name="shadowCopyDirectory" value="../ShadowCopyDirectory/" />
        <!-- Only enable handler logging if you encounter issues-->
        <!--<handlerSetting name="debugFile" value=".\logs\aspnetcore-debug.log" />-->
        <!--<handlerSetting name="debugLevel" value="FILE,TRACE" />-->
      </handlerSettings>
    </aspNetCore>
  </system.webServer>
</configuration>
نظرات مطالب
Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها
- به عنوان مثال در بسیاری از متدهای لایه سرویس نیاز به چنین بررسی می‌باشد و ابتدای متدها این قضیه ابتدا چک می‌شود. این متد به عنوان یک متد Domain Service صرفا در لایه سرویس استفاده می‌شود. 
        [Transactional]
        public async Task CreateAsync(OrganizationalUnitCreateModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            if (model.ParentId.HasValue)
                await _manager.CheckIsDeactiveAsync(model.ParentId.Value).ConfigureAwait(false);
            //...

        }

در پاسخ به ادامه سوال یک باید عرض کنم بله وقتی کاربری یافت نشد چه کاری می‌توانیم انجام دهیم؟ وظیفه متد مورد نظر بازگشت دادن یک کاربر بود نه مقدار Null و در اینجا صرفا یک Exception سفارشی صادر شده تا در بالاترین لایه مدیریت شود و پیغامی مناسب به کاربر نشان داده شود یا ...
توجه شما را جلب می‌کنم به مطلب :نکات کار با استثناءها در دات نت
به این چنین کدهایی معمولاً The null cancer گفته می‌شود (سرطان نال!) زیرا اجازه داده‌ایم متد، خروجی null را بازگشت دهد. 
- خیلی موافق نیستم با صحبت شما؛ من از استثنا‌ها برای ارسال پیغامی از داخلی‌ترین لایه به خارجی‌ترین لایه استفاده می‌کنم بجای بازگشت خروجی مثل OperationResult به لایه‌های بالاتر به صورت زنجیره ای. رکورد کاربری در دیتبایس موجود نیست را شما پیش بینی می‌کنید؟ من در متن مطلب هم اشاره کردم به عنوان ابزاری برای هدف خاصی استفاده می‌کنم از استثنا ها.
- بله درست است. ولی این الگوهایی که نام بردید در یک پروژه بزرگ تعداد خط کد را خیلی بالا خواهد برد. 
این روش برای مدیریت این چنین کارها به مراتب خیلی ساده بوده و بحث fail-fast را دنبال می‌کند.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config
یک نکته‌ی تکمیلی:  ارتقاء به ASP.NET Core 2.0 

بر اساس مستندات دات نت Core 2.0،  در فایل Startup.cs، الزاما نیازی به تنظیمات ذیل
public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }
نیست و با جایگزین کردن این قطعه کد با
public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration)
{
     Configuration = configuration;
}
و سپس انجام این تغییرات
public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
در فایل Program.cs موجب فراخوانی کلیه تنظیمات پیش فرض خواهد شد.
در واقع متد CreateDefaultBuilder این کار را انجام می‌دهد. البته همچنان امکان تنظیمات سفارشی نیز موجود است.
public static IWebHost BuildWebHost(string[] args)
{
  return WebHost.CreateDefaultBuilder()
    .ConfigureAppConfiguration((ctx, cfg) =>
    {
      cfg.SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("config.json", true) // require the json file!
        .AddEnvironmentVariables();
    })
    .ConfigureLogging((ctx, logging) => { }) // No logging
    .UseStartup<Startup>()
    .Build();
}

لازم به ذکر است سرویس IConfiguration از ابتدا در سیستم ثبت شده است و جهت دسترسی به تنظیمات میتوان در قسمت‌های مختلف برنامه آن را تزریق نمود.