نظرات مطالب
استفاده از pjax بجای ajax در ASP.NET MVC
در مثالی که زده شد، فقط لینک‌هایی که دارای ویژگی withpjax هستند تحت کنترل این افزونه قرار می‌گیرند و نه هیچ لینک دیگری در برنامه و نه هیچ روش بازگشت دیگری:
  $(document).pjax('a[withpjax]', '#pjaxContainer', { timeout: 5000 });
البته این یک مثال است و اگر مثلا withpjax آن‌را حذف کنید:
$(document).pjax('a', '#pjaxContainer', { timeout: 5000 });
تمام لینک‌های صفحه تحت کنترل خواهند بود. شبیه به حالتی که عنوان کردید صفحه بندی گرید به هم خورده. برای اینکه این نوع تداخل‌ها رخ ندهند و هر لینکی در صفحه توسط این افزونه پردازش نشود، بهتر است از روش پیشنهادی استفاده کنید.
نظرات مطالب
بهینه سازی فایلهای js و css در برنامه‌های ASP.NET با استفاده از Combres - قسمت اول
احتمالا تو نوع آدرس دهی یکی از ResourceSet‌های فایل xml یک مشکلی وجود داره! (بهتر بود نمونه کد یا پروژه اینجا قرار میدادید تا بهتر بتونم کمکتون کنم)
نظرات مطالب
ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax
- در ASP.NET Core، یکسری key/value pair مانند HttpContext.Request.Query و HttpContext.Request.Form و ... به صورت مجزایی وجود دارند. البته روش بهتر استفاده از Model Binding است.
+ سمت سرور مطلب جاری در ASP.NET Core، با سمت سرور مطلب «بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core» یکی هست. آن مطلب را از قسمت «دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌» به بعد مطالعه کنید.
+  نکته‌ی تکمیلی «در نسخه‌ی اخیر افزونه jquery-ajax-unobtrusive، ارسال فایل با استفاده از FormData نیز به صورت توکار پشتیبانی می‌شود» را هم مدنظر داشته باشید. یعنی برای سمت کلاینت نیازی به نوشتن کدهای خاصی برای ارسال فایل به سمت سرور نیست (نیازی نیست کد جی‌کوئری بنویسید). همینکه به صورت معمولی از افزونه‌ی jquery-ajax-unobtrusive استفاده کنید، تمام نکات آن مانند قبل است.
مطالب
اطلاع از بروز رسانی نرم افزار ساخته شده
برای شما هم پیش آمده که نرم افزاری را تهیه و منتشر کرده باشید و تمایل داشته باشید که استفاده کنندگان از وجود نسخه بروز شده مطلع شوند. یک راه ساده این است که اطلاعات نسخه جدید نرم افزار را داخل فایلی ذخیره کنیم و در وب سایت پشتیبانی نرم افزار قرار دهیم. حال بایستی اطلاعات این فایل را در زمان اجرای برنامه بررسی کنیم و در صورت وجود نسخه جدید از نرم افزار به کاربر اطلاع رسانی کنیم.
ابتدا فایل اطلاعات بروز رسانی نرم افزار را تهیه می‌کنیم و در وب سایت پشتیبانی نرم افزار قرار میدهیم. در اینجا از قالب Xml استفاده شده. که در آن Version نسخه در دسترس نرم افزار است و URL هم مسیر وب سایت و یا فایل بروز رسانی است. 
<?xml version="1.0" encoding="utf-8"?>
<AccountingApplication>
  <Version>1.5.2</Version>
  <URL>http://www.myappsupport.ir</URL>
</AccountingApplication>
نرم افزار را ساخته و کد زیر را در محل مناسبی کد نویسی می‌کنیم. این کد در ابتدا فایل Xml را خوانده و اطلاعات مورد نیاز را از آن دریافت می‌کند. سپس با استخراج نسخه اسمبلی برنامه و مقایسه این دو با هم از وجود نسخه جدید نرم افزار مطلع میشود.  
...
using System.Xml;
namespace CheckUpdateApplication
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void CheckUpdate_Click(object sender, EventArgs e)
        {
            Version NewVersion = null;
            string DownloadPath = "";
            try
            {
                XmlTextReader xmlRead = new XmlTextReader("http://www.myappsupport.ir/AccUpdateVersion.xml");
                xmlRead.MoveToContent();
                string elmName = "";
                if ((xmlRead.NodeType == XmlNodeType.Element) && (xmlRead.Name == "AccountingApplication"))
                {
                    while (xmlRead.Read())
                    {
                        if (xmlRead.NodeType == XmlNodeType.Element)
                        {
                            elmName = xmlRead.Name;
                        }
                        else 
                        {
                            if ((xmlRead.NodeType == XmlNodeType.Text) && (xmlRead.HasValue))
                            {
                                switch (elmName)
                                {
                                    case "Version":
                                        NewVersion = new Version(xmlRead.Value);
                                        break;
                                    case "URL":
                                        DownloadPath = xmlRead.Value;
                                        break;
                                }
                            }
                        }
                    }
                }
                Version AppVertion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
                if (AppVertion.CompareTo(NewVersion) < 0)
                {
                    DialogResult Result = MessageBox.Show("نسخه " + 
                        NewVersion.Major.ToString() + "." + 
                        NewVersion.Minor.ToString() + "." + 
                        NewVersion.Build.ToString() + " در دسترس میباشد مایل به دانلود هستید؟", "نسخه جدید", 
                        MessageBoxButtons.YesNo,MessageBoxIcon.Question);
                    if (Result == DialogResult.Yes)
                    {
                        System.Diagnostics.Process.Start(DownloadPath);
                    }
                }
                else
                {
                    MessageBox.Show("نرم افزار بروز میباشد");
                }
            }
            catch (Exception E)
            {
                MessageBox.Show(E.Message); 
            }
        }
    }
}

به روش زیر هم نسخه اسمبلی برنامه را می‌شود تغییر داد.

سورس برنامه نمونه
پاسخ به بازخورد‌های پروژه‌ها
فضای نام MVC
سلام؛ ممنون
حقیقتش من یک صفحه در codeplex برای این پروژه ایجاد کردم ولی چند دفعه موقع commit کردن پروژه، با مشکل مواجه می‌شوم و احتمالا دلیلش حجم بالای پروژه( نزدیک به 60 مگابایت) است.
ولی باز من پروژه را اصلاح کردم و این اصلاحات شامل تمامی موارد گزارش شده و یکم تمیز سازی کد و فضای نام‌ها و وابستگی به کتابخانه‌ی persia که اشتباها  در دو پروژه به آدرس مطلق در کامپیوترم رفرنس داده شده بود، می‌شود.
اکنون نیز می‌توانید نسخه‌ی جدید را از همین سایت در بخش فایل‌های پروژه دانلود کنید.
ولی باز هم اگر کسی از دوستان در اجرای پروژه مشکل داشت، همین جا اعلام کند تا پروژه را به صورت یکجا در هاست خودم آپلود کنم و اینجا  قرار بدهم.
موفق باشید...
مطالب
تولید و ارسال خودکار بسته‌های NuGet پروژه‌های NET Core. به کمک AppVeyor
اگر پروژه‌ی شما به همراه توزیع بسته‌های نیوگت است، پس از مدتی، از build و آپلود دستی بسته‌های نیوگت آن‌ها خسته خواهید شد. همچنین این سؤال هم برای مصرف کنندگان بسته‌ی نیوگت شما همواره وجود خواهد داشت: «آیا بسته‌ی نهایی را که آپلود کرده، دقیقا بر اساس سورس کد موجود در مخزن کد عمومی آن تهیه شده‌است؟»
برای رفع این مشکلات، از روش‌های توسعه‌ی به همراه ابزارهای یکپارچگی مداوم استفاده می‌شود. برای نمونه، AppVeyor یکی از سرویس‌های ابری یکپارچگی مداوم (Continuous Integration و یا به اختصار CI) است. به کمک آن می‌توان یک image از ویندوز سرور را به همراه ابزارهای build، آزمایش و توزیع برنامه‌های NET. در اختیار داشت. این سرویس، مخزن کد شما را مونیتور کرده و هر زمانیکه تغییری را در آن ایجاد کردید، آن‌ها را به صورت خودکار build و در صورت موفقیت آمیز بودن این عملیات، بسته‌ی نیوگت متناظری را به سایت nuget.org ارسال می‌کند. بنابراین پس از یکپارچه کردن مخزن کد خود با این نوع سرویس‌های یکپارچگی مداوم، دیگر حتی نیازی به build دستی آن نیز نخواهید داشت. همینقدر که کدی را به مخزن کد تحت نظر، commit کنید، مابقی مراحل آن خودکار است.
به همین جهت در این مطلب قصد داریم نحوه‌ی اتصال یک مخزن کد GitHub را به سرویس یکپارچگی مداوم AppVeyor، جهت تولید خودکار بسته‌های Nuget، بررسی کنیم.




معرفی سرویس ابری AppVeyor

AppVeyor یک راه حل یکپارچگی مداوم چند سکویی است که استفاده‌ی از آن برای پروژه‌های سورس باز رایگان است و سازگاری فوق العاده‌ای را با محصولات مایکروسافت دارد. برای ورود به آن می‌توان از اکانت‌های GitHub ،BitBucket و VSTS (Visual Studio Team Services) استفاده کرد.
گردش کاری متداول یکپارچگی مداوم AppVeyor به این صورت است:
الف) با اکانت GitHub خود به آن وارد شوید.
ب) یک مخزن کد GitHub خود را به آن Import کنید.
ج) به مخزن کد GitHub خود یک فایل yml. تنظیمات مخصوص AppVeyor را اضافه کنید.
د) نظاره‌گر Build و توزیع خودکار پروژه‌ی خود باشید.


ایجاد اکانت و اتصال به مخزن کد GitHub

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


پس از ورود موفق، گزینه‌ی new project را انتخاب کنید:


در ادامه مخزن کد GitHub و نوع عمومی آن‌را انتخاب می‌کنیم تا AppVeyor بتواند پروژه‌های آن‌را Import کند و همچنین به آن‌ها web hookهایی را اضافه کند تا با اعمال تغییراتی در سمت GitHub، کار اطلاع رسانی آن‌ها به AppVeyor به صورت خودکار صورت گیرد:


پس از آن لیست مخزن‌های کد شما در همینجا ارائه می‌شود تا بتوانید یک یا چند مورد را انتخاب کنید:


انجام تنظیمات عمومی مخزن کد

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


در اینجا اگر پروژه‌ی شما از نوع NET Core. است، گزینه‌ی NET Core .csproj patching. را انتخاب نمائید:


سپس در پایین صفحه بر روی دکمه‌ی Save کلیک کنید.


انتخاب و تنظیم محیط Build

در ادامه در برگه‌ی settings و سپس از منوی کنار صفحه‌ی آن، گزینه‌ی Environment را انتخاب کنید:


در این صفحه، worker image را بر روی VS 2017 قرار دهید و همچنین در قسمت Cached directories and files، مسیر C:\Users\appveyor\.dnx را جهت کش کردن عملیات Build و بالا بردن سرعت آن، مقدار دهی کنید. سپس در پایین صفحه بر روی دکمه‌ی Save کلیک نمائید.

اکنون بر روی گزینه‌ی Build در منوی سمت چپ صفحه کلیک کنید. در اینجا سه حالت msbuild ،script و off را می‌توان انتخاب کرد.
- در حالت msbuild، که ساده‌ترین حالت ممکن است، فایل sln. مخزن کد، یافت شده و بر اساس آن به صورت خودکار تمام پروژه‌های این solution یکی پس از دیگری build خواهند شد. این مورد برای برنامه‌های Full .NET Framework شاید گزینه‌ی مناسبی باشد.
- حالت script برای پروژه‌های NET Core. مناسب‌تر است و در این حالت می‌توان کنترل بیشتری را بر روی build داشت. به علاوه این روش بر روی لینوکس هم کار می‌کند؛ زیرا در آنجا دسترسی به msbuild نداریم.
- حالت off به معنای خاموش کردن عملیات build است.

در اینجا گزینه‌ی cmd را جهت ورود build script انتخاب می‌کنیم:


سپس دستورات ذیل را جهت ورود به پوشه‌ی پروژه‌ی کتابخانه (جائیکه فایل csproj آن قرار دارد)، بازیابی وابستگی‌های پروژه و سپس تولید بسته‌ی نیوگتی از آن، وارد می‌کنیم:
cd ./src/DNTPersianUtils.Core
dotnet restore
dotnet pack -c release
cd ../..
در ادامه در پایین صفحه بر روی دکمه‌ی Save کلیک نمائید.
ذکر  ../.. cd  در انتهای این دستورات ضروری است. در غیر اینصورت cd بعدی در تنظیماتی دیگر، داخل همین پوشه انجام می‌شود.


تنظیم اجرای خودکار آزمون‌های واحد


در همین صفحه، گزینه‌های settings -> tests -> script -> cmd را انتخاب و سپس دستورات زیر را وارد کرده و آن‌را ذخیره کنید:
cd ./src/DNTPersianUtils.Core.Tests
dotnet restore
dotnet test
cd ../..
به این ترتیب به صورت خودکار آزمون‌های واحد موجود در پروژه‌ی انتخابی، توسط NET Core CLI. اجرا خواهند شد.


تنظیم اطلاع رسانی خودکار از اجرای عملیات


در برگه‌ی settings -> notifications مطابق تنظیمات فوق می‌توان نوع email را جهت اطلاع رسانی شکست انجام عملیات یکپارچگی مداوم، انتخاب کرد.


آزمایش Build خودکار

برای آزمایش تنظیماتی که انجام دادیم، به برگه‌ی latest build مراجعه کرده و بر روی دکمه‌ی new build کلیک کنید تا اسکریپت‌های build و test فوق اجرا شوند. بدیهی است اجرای بعدی این اسکریپت‌ها خودکار بوده و به ازای هر commit به GitHub، بدون نیاز به مراجعه‌ی مستقیم به appveyor صورت می‌گیرند.



اضافه کردن نماد AppVeyor به پروژه

در تنظیمات برگه‌ی Settings، گزینه‌ی AppVeyor badge نیز در منوی سمت چپ صفحه، وجود دارد:


در اینجا همان کدهای mark down آن‌را انتخاب کرده و به ابتدای فایل readme پروژه‌ی خود اضافه کنید. برای نمونه نماد فعلی (تصویر فوق)، build failing را نمایش می‌دهد؛ چون سه آزمون واحد آن مشکل دارند و باید اصلاح شوند.
پس از رفع مشکلات پروژه و commit آن‌ها، build و اجرای خودکار آزمون‌های واحد آن توسط AppVeyor صورت گرفته و اینبار این نماد به صورت زیر تغییر می‌کند:



ارسال خودکار بسته‌ی نیوگت تولید شده به سایت nuget.org

برای ارسال خودکار حاصل Build، به سایت نیوگت، نیاز است یک API Key داشته باشیم. به همین جهت به صفحه‌ی مخصوص آن در سایت nuget پس از ورود به سایت آن، مراجعه کرده و یک کلید API جدید را صرفا برای این پروژه تولید کنید (در قسمت Available Packages بسته‌ی پیشینی را که دستی آپلود کرده بودید انتخاب کنید).
پس از کپی کردن کلید تولید شده‌ی در سایت nuget، به قسمت settings -> deployment مراجعه کرده و یک تامین کننده‌ی جدید از نوع nuget را اضافه کنید:


در اینجا API Key را ذکر خواهیم کرد. سپس در پایین صفحه بر روی دکمه‌ی Save کلیک کنید.

همچنین نیاز است مشخص کنیم که بسته‌های nupkg تولید شده در چه مسیری قرار دارند. به همین جهت در قسمت settings -> artifacts مسیر پوشه‌ی bin نهایی را ذکر می‌کنیم:


این مورد را نیز با کلیک بر روی دکمه‌ی Save ذخیره کنید.

اکنون اگر نگارش جدیدی را به GitHub ارسال کنیم (تغییر VersionPrefix در فایل csproj و سپس commit آن)، پس از Build پروژه، بسته‌ی نیوگت آن نیز به صورت خودکار تولید شده و به سایت nuget.org ارسال می‌شود. لاگ آن‌را در پایین صفحه‌ی برگه‌ی latest build می‌توانید مشاهده کنید.



ساده سازی مراحل تنظیمات AppVeyor

در صفحه‌ی settings‌، در منوی سمت چپ آن، گزینه‌ی export YAML نیز وجود دارد. در اینجا می‌توان تمام تنظیمات انجام شده‌ی فوق را با فرمت yml. دریافت کرد و سپس این فایل را به ریشه‌ی مخزن کد خود افزود. با وجود این فایل، دیگر نیازی به طی کردن دستی هیچکدام از مراحل فوق نیست.
برای نمونه فایل appveyor.yml نهایی مطابق با توضیحات این مطلب، چنین محتوایی را دارد:
version: 1.0.{build}
image: Visual Studio 2017
dotnet_csproj:
  patch: true
  file: '**\*.csproj'
  version: '{version}'
  package_version: '{version}'
  assembly_version: '{version}'
  file_version: '{version}'
  informational_version: '{version}'
cache: C:\Users\appveyor\.dnx
build_script:
- cmd: >-
    cd ./src/DNTPersianUtils.Core

    dotnet restore

    dotnet pack -c release

    cd ../..
test_script:
- cmd: >-
    cd ./src/DNTPersianUtils.Core.Tests

    dotnet restore

    dotnet test

    cd ../..
artifacts:
- path: ./src/DNTPersianUtils.Core/bin/release/*.nupkg
  name: NuGet
deploy:
- provider: NuGet
  api_key:
    secure: xyz
  skip_symbols: true
notifications:
- provider: Email
  to:
  - me@yahoo.com
  on_build_success: false
  on_build_failure: true
  on_build_status_changed: true
بنابراین بجای طی مراحل عنوان شده‌ی در این بحث می‌توانید یک فایل appveyor.yml را با محتوای فوق (پس از اصلاح مسیرها و نام‌ها) در ریشه‌ی پروژه‌ی خود قرار دهید و ... صرفا مخزن کد آن‌را در appveyor ثبت و import کنید. مابقی مراحل یکپارچگی مداوم آن خودکار است و نیاز به تنظیم دیگری ندارد. فقط برای آزمایش آن به برگه‌ی Latest build مراجعه کرده و یک build جدید را شروع کنید تا مطمئن شوید همه چیز به درستی کار می‌کند.
یک نکته: api_key ذکر شده‌ی در اینجا در قسمت secure، رمزنگاری شده‌است. برای تولید آن می‌توانید از مسیر https://ci.appveyor.com/tools/encrypt استفاده کنید. به این صورت مشکلی با عمومی کردن فایل appveyor.yml خود در GitHub نخواهید داشت.
مطالب
روش کار با فایل‌های پویای ارائه شده‌ی توسط یک برنامه‌ی ASP.NET Core در برنامه‌های React
پس از آشنایی با «روش کار با فایل‌های ایستا در برنامه‌های React»، اکنون اگر این فایل‌ها ایستا نباشند و توسط یک برنامه‌ی ASP.NET Core بازگشت داده شوند، چطور می‌توان از آن‌ها در برنامه‌های React استفاده کرد؟

برپایی پروژه‌های مورد نیاز

ابتدا یک پوشه‌ی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا می‌کنیم:
> dotnet new react
در مورد این قالب که امکان تجربه‌ی توسعه‌ی یکپارچه‌ی ASP.NET Core و React را میسر می‌کند، در مطلب «روش یکی کردن پروژه‌های React و ASP.NET Core» بیشتر بحث کردیم.
سپس در این پوشه، پوشه‌ی ClientApp پیش‌فرض آن‌را حذف می‌کنیم؛ چون کمی قدیمی است. همچنین فایل‌های کنترلر و سرویس آب و هوای پیش‌فرض آن‌را به همراه پوشه‌ی صفحات Razor آن، حذف می‌کنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
از تنظیم زیر استفاده کرده‌ایم تا با هر بار تغییری در کدهای پروژه‌ی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
بدیهی است در این حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود.

اکنون در ریشه‌ی پروژه‌ی ASP.NET Core ایجاد شده، دستور زیر را صادر می‌کنیم تا پروژه‌ی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
سپس وارد این پوشه‌ی جدید شده و بسته‌های زیر را نصب می‌کنیم:
> cd clientapp
> npm install --save bootstrap axios
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود که برای افزودن فایل bootstrap.css آن به پروژه‌ی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
 import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.
- برای دریافت فایل‌ها از سمت سرور، از کتابخانه‌ی معروف axios استفاده خواهیم کرد.


کدهای سمت سرور دریافت فایل‌های پویا

در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت می‌دهند. این محتوای باینری می‌تواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc;

namespace DownloadFilesSample.Controllers
{
    [Route("api/[controller]")]
    public class ReportsController : Controller
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            return File(virtualPath: "~/app_data/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
فایل بازگشتی فوق که در این مثال در مسیر wwwroot\app_data\sample.pdf برنامه‌ی وب کپی شده‌است، در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.


روش دریافت محتوای باینری در برنامه‌های React

برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایل‌های PDF و اکسل و غیره، مهم‌ترین نکته، تنظیم ویژگی responseType آن به blob است:
  const getResults = async () => {
      const { headers, data } = await axios.get(apiUrl, {
        responseType: "blob"
      });
  }


ساخت URL برای دسترسی به اطلاعات باینری

تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی می‌کنند. این متد، شیء URL را از شیء window جاری دریافت می‌کند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید می‌کند. حاصل آن، یک URL ویژه‌است مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شده‌ی از سرور را به این URL ویژه، مشاهده می‌کنید:
import axios from "axios";
import React, { useEffect, useState } from "react";

export default function DisplayPdf() {
  const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport";

  const [blobInfo, setBlobInfo] = useState({
    blobUrl: "",
    fileName: ""
  });

  useEffect(() => {
    getResults();
  }, []);

  const getResults = async () => {
    try {
      const { headers, data } = await axios.get(apiUrl, {
        responseType: "blob"
      });
      console.log("headers", headers);

      const pdfBlobUrl = window.URL.createObjectURL(data);
      console.log("pdfBlobUrl", pdfBlobUrl);

      const fileName = headers["content-disposition"]
        .split(";")
        .find(n => n.includes("filename="))
        .replace("filename=", "")
        .trim();
      console.log("filename", fileName);

      setBlobInfo({
        blobUrl: pdfBlobUrl,
        fileName: fileName
      });
    } catch (error) {
      console.log(error);
    }
  };
توضیحات:
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شده‌ایم. به این معنا که متد getResults فراخوانی شده‌ی در آن، پس از رندر کامپوننت در DOM فراخوانی می‌شود و بهترین محلی است که از آن می‌توان برای ارسال درخواست‌های Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کرده‌ایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آن‌را تبدیل به یک blob URL کرده‌ایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کرده‌ایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر می‌شود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
 بنابراین می‌توان آن‌را تجزیه کرد و سپس filename را از آن استخراج نمود.
- اکنون که نام فایل و URL دسترسی به داده‌ی فایل باینری دریافتی از سرور را استخراج و ایجاد کرده‌ایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن می‌توان برای مثال فایل PDF دریافتی را نمایش داد.


نمایش فایل PDF دریافتی از سرور، به همراه دکمه‌های دریافت، چاپ و بازکردن آن در برگه‌ای جدید

در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده می‌کنید:
import axios from "axios";
import React, { useEffect, useState } from "react";

export default function DisplayPdf() {

  // ...

  const { blobUrl } = blobInfo;

  return (
    <>
      <h1>Display PDF Files</h1>
      <button className="btn btn-info" onClick={handlePrintPdf}>
        Print PDF
      </button>
      <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}>
        Show PDF in a new tab
      </button>
      <button className="btn btn-success ml-2" onClick={handleDownloadPdf}>
        Download PDF
      </button>

      <section className="card mb-5 mt-3">
        <div className="card-header">
          <h4>using iframe</h4>
        </div>
        <div className="card-body">
          <iframe
            title="PDF Report"
            width="100%"
            height="600"
            src={blobUrl}
            type="application/pdf"
          ></iframe>
        </div>
      </section>

      <section className="card mb-5">
        <div className="card-header">
          <h4>using object</h4>
        </div>
        <div className="card-body">
          <object
            data={blobUrl}
            aria-label="PDF Report"
            type="application/pdf"
            width="100%"
            height="100%"
          ></object>
        </div>
      </section>

      <section className="card mb-5">
        <div className="card-header">
          <h4>using embed</h4>
        </div>
        <div className="card-body">
          <embed
            aria-label="PDF Report"
            src={blobUrl}
            type="application/pdf"
            width="100%"
            height="100%"
          ></embed>
        </div>
      </section>
    </>
  );
}
که چنین خروجی را ایجاد می‌کند:


در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، می‌توان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیت‌های توکار مرورگر صورت می‌گیرد و نیاز به نصب افزونه‌ی خاصی را ندارد.

در ادامه کدهای مرتبط با سه دکمه‌ی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده می‌کنید.


مدیریت دکمه‌ی چاپ PDF

پس از اینکه به blobUrl دسترسی یافتیم، اکنون می‌توان یک iframe مخفی را ایجاد کرد، سپس src آن‌را به این آدرس ویژه تنظیم نمود و در آخر متد print آن‌را فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر می‌شود:
  const handlePrintPdf = () => {
    const { blobUrl } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = blobUrl;
    document.body.appendChild(iframe);
    if (iframe.contentWindow) {
      iframe.contentWindow.print();
    }
  };


مدیریت دکمه‌ی نمایش فایل PDF در یک برگه‌ی جدید

اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگه‌ای جدید نمایش دهید، می‌توان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => {
    const { blobUrl } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    window.open(blobUrl);
  };

مدیریت دکمه‌ی دریافت فایل PDF

بجای نمایش فایل PDF می‌توان دکمه‌ای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
  const handleDownloadPdf = () => {
    const { blobUrl, fileName } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = blobUrl;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
  };
در اینجا یک anchor جدید به صورت مخفی به صفحه اضافه می‌شود که href آن به blobUrl تنظیم شده‌است و همچنین از فایل fileName استخراجی نیز در اینجا جهت ارائه‌ی نام اصلی فایل دریافتی از سرور، کمک گرفته شده‌است. سپس متد click آن فراخوانی خواهد شد. این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایل‌های PDF نیست.
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، می‌توان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگی‌های سمت کلاینت نیز می‌شود، ابتدا به پوشه‌ی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار می‌کند. سپس به پوشه‌ی اصلی برنامه‌ی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائه‌ی برنامه‌ی React بر روی پورت 5001 می‌شود.
مطالب
اتصال Node.js به SQL Server با استفاده از Edge.js
اگر خواسته باشید که با استفاده از Node.js به SQL Server متصل شوید، احتمالا متوجه شده‌اید ماژولی که مایکروسافت منتشر کرده است، ناقص بوده و به صورت پیش نمایش است که بسیاری از ویژگی‌ها و مسائل مهم، در آن در نظر گرفته نشده است.

یکی دیگر از ماژول‌هایی که امکان اتصال Node.js را به SQL Server ممکن می‌کند، Edge.js است. Edge.js یک ماژول Node.js است که امکان اجرای کدهای دات نت را در همان پروسه توسط Node.js فراهم می‌کند. این مسئله، توسعه دهندگان Node.js را قادر می‌سازد تا از فناوری‌هایی که به صورت سنتی استفاده‌ی از آنها سخت یا غیر ممکن بوده است را به راحتی استفاده کنند. برای نمونه:
  • SQL Server
  • Active Directory
  • Nuget packages
  • استفاده از سخت افزار کامپیوتر (مانند وب کم، میکروفن و چاپگر)


نصب Node.js

اگر Node.js را بر روی سیستم خود نصب ندارید، می‌توانید از اینجا آن را دانلود کنید. بعد از نصب برای اطمینان از کارکرد آن، command prompt را باز کرده و دستور زیر را تایپ کنید:

node -v
شما باید نسخه‌ی نصب شده‌ی Node.js را مشاهده کنید.

ایجاد پوشه پروژه

سپس پوشه‌ای را برای پروژه Node.js خود ایجاد کنید. مثلا با استفاده از command prompt و دستور زیر:

md \projects\node-edge-test1
cd \projects\node-edge-test1

نصب Edge.js

Node با استفاده از package manager خود دانلود و نصب ماژول‌ها را خیلی آسان کرده است. برای نصب، در command prompt عبارت زیر را تایپ کنید:

npm install edge
npm install edge-sql
فرمان اول باعث نصب Edge.js و دومین فرمان سبب نصب پشتیبانی از SQL Server می‌شود.

Hello World

ایجاد یک فایل متنی با نام server.js و نوشتن کد زیر در آن:
var edge = require('edge');

// The text in edge.func() is C# code
var helloWorld = edge.func('async (input) => { return input.ToString(); }');

helloWorld('Hello World!', function (error, result) {
    if (error) throw error;
    console.log(result);
});
حالا برای اجرای این Node.js application از طریق command prompt کافی است به صورت زیر عمل کنید:
node server.js
همانطور که مشاهده می‌کنید "!Hello World" در خروجی چاپ شد.

ایجاد پایگاه داده تست

در مثال‌های بعدی، نیاز به یک پایگاه داده داریم تا query‌ها را اجرا کنیم. در صورتی که SQL Server بر روی سیستم شما نصب نیست، می‌توانید نسخه‌ی رایگان آن را از اینجا دانلود و نصب کنید. همچنین SQL Management Studio Express را نیز نصب کنید.

  1. در SQL Management Studio، یک پایگاه داده را با نام node-test با تنظیمات پیش فرض ایجاد کنید.
  2. بر روی پایگاه داده node-test راست کلیک کرده و New Query را انتخاب کنید.
  3. اسکریپت زیر را copy کرده و در آنجا paste کنید، سپس بر روی Execute کلیک کنید.
IF EXISTS(SELECT 1 FROM sys.tables WHERE object_id = OBJECT_ID('SampleUsers')) BEGIN; DROP TABLE SampleUsers; END; GO

CREATE TABLE SampleUsers ( Id INTEGER NOT NULL IDENTITY(1, 1), FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Email VARCHAR(255) NOT NULL, CreateDate DATETIME NOT NULL DEFAULT(getdate()), PRIMARY KEY (Id) ); GO

INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Orla','Sweeney','nunc@convallisincursus.ca','Apr 13, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Zia','Pickett','porttitor.tellus.non@Duis.com','Aug 31, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Justina','Ayala','neque.tellus.imperdiet@temporestac.com','Jul 28, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Levi','Parrish','adipiscing.elit@velarcueu.com','Jun 21, 2014');
INSERT INTO SampleUsers(FirstName,LastName,Email,CreateDate) VALUES('Pearl','Warren','In@dignissimpharetra.org','Mar 3, 2014');
نتیجه‌ی اجرای کد بالا، ایجاد جدولی با نام SampleUsers و درج 5 رکورد در آن می‌شود.

تنظیمات ConnectionString

قبل از استفاده از Edge.js با SQL Server، باید متغیر محیطی (environment variable) با نام EDGE_SQL_CONNECTION_STRING را تعریف کنید.

set EDGE_SQL_CONNECTION_STRING=Data Source=localhost;Initial Catalog=node-test;Integrated Security=True
این متغیر تنها برای command prompt جاری تعریف شده است و با بستن آن از دست می‌رود. در صورتیکه از Node.js Tools for Visual Studio استفاده می‌کنید، نیاز به ایجاد یک متغیر محیطی دائمی و راه اندازی مجدد VS دارید. همچنین در صورتیکه بخواهید متغیر محیطی دائمی ایجاد کنید، فرمان زیر را اجرا کنید:
SETX EDGE_SQL_CONNECTION_STRING "Data Source=localhost;Initial Catalog=node-test;Integrated Security=True"


روش اول: اجرای مستقیم SQL Server Query در Edge.js

فایلی با نام server-sql-query.js را ایجاد کرده و کد زیر را در آن وارد کنید:

var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

var getTopUsers = edge.func('sql', function () {/*
    SELECT TOP 3 * FROM SampleUsers ORDER BY CreateDate DESC
*/});

function logError(err, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.write("Error: " + err);
    res.end("");
}    

http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/html' });

    getTopUsers(null, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
        }
    });
}).listen(port);
console.log("Node server listening on port " + port);
سپس با استفاده از command prompt، فرمان زیر را اجرا کنید:
node server-sql-query.js
حال مرورگر خود را باز و سپس آدرس http://localhost:8080 را باز کنید. در صورتی که همه چیز به درستی انجام گرفته باشد لیستی از 3 کاربر را خواهید دید.

روش دوم: اجرای کد دات نت برای SQL Server Query

Edge.js تنها از دستورات Update، Insert، Select و Delete پشتیبانی می‌کند. در حال حاضر از store procedures و مجموعه‌ای از کد SQL پشتیبانی نمی‌کند. بنابراین، اگر چیزی بیشتر از عملیات CRUD می‌خواهید انجام دهید، باید از دات نت برای این کار استفاده کنید.

یادتان باشد، همیشه async

مدل اجرایی Node.js به صورت یک حلقه‌ی رویداد تک نخی است. بنابراین این بسیار مهم است که کد دات نت شما به صورت async باشد. در غیر اینصورت یک فراخوانی به دات نت سبب مسدود شدن و ایجاد خرابی در Node.js می‌شود.

ایجاد یک Class Library

اولین قدم، ایجاد یک پروژه Class Library در Visual Studio که خروجی آن یک فایل DLL است و استفاده از آن در Edge.js است. پروژه Class Library با عنوان EdgeSampleLibrary ایجاد کرده و فایل کلاسی با نام Sample1 را به آن اضافه کنید و سپس کد زیر را در آن وارد کنید:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;

namespace EdgeSampleLibrary
{
     public class Sample1
    {
        public async Task<object> Invoke(object input)
        {
            // Edge marshalls data to .NET using an IDictionary<string, object>
            var payload = (IDictionary<string, object>) input;
            var pageNumber = (int) payload["pageNumber"];
            var pageSize = (int) payload["pageSize"];
            return await QueryUsers(pageNumber, pageSize);
        }

        public async Task<List<SampleUser>> QueryUsers(int pageNumber, int pageSize)
        {
            // Use the same connection string env variable
            var connectionString = Environment.GetEnvironmentVariable("EDGE_SQL_CONNECTION_STRING");
            if (connectionString == null)
                throw new ArgumentException("You must set the EDGE_SQL_CONNECTION_STRING environment variable.");

            // Paging the result set using a common table expression (CTE).
            // You may rather do this in a stored procedure or use an 
            // ORM that supports async.
            var sql = @"
DECLARE @RowStart int, @RowEnd int;
SET @RowStart = (@PageNumber - 1) * @PageSize + 1;
SET @RowEnd = @PageNumber * @PageSize;

WITH Paging AS
(
    SELECT  ROW_NUMBER() OVER (ORDER BY CreateDate DESC) AS RowNum,
            Id, FirstName, LastName, Email, CreateDate
    FROM    SampleUsers
)
SELECT  Id, FirstName, LastName, Email, CreateDate
FROM    Paging
WHERE   RowNum BETWEEN @RowStart AND @RowEnd
ORDER BY RowNum;
";
            var users = new List<SampleUser>();

            using (var cnx = new SqlConnection(connectionString))
            {
                using (var cmd = new SqlCommand(sql, cnx))
                {
                    await cnx.OpenAsync();

                    cmd.Parameters.Add(new SqlParameter("@PageNumber", SqlDbType.Int) { Value = pageNumber });
                    cmd.Parameters.Add(new SqlParameter("@PageSize", SqlDbType.Int) { Value = pageSize });

                    using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
                    {
                        while (await reader.ReadAsync())
                        {
                            var user = new SampleUser
                            {
                                Id = reader.GetInt32(0), 
                                FirstName = reader.GetString(1), 
                                LastName = reader.GetString(2), 
                                Email = reader.GetString(3), 
                                CreateDate = reader.GetDateTime(4)
                            };
                           users.Add(user);
                        }
                    }
                }
            }
            return users;
        } 
    }

    public class SampleUser
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public DateTime CreateDate { get; set; }
    }
}
سپس ذخیره و کامپایل کنید. فایل DLL خروجی که در مسیر
[project]/bin/Debug/EdgeSampleLibrary.dll
قرار دارد را در پوشه‌ی پروژه Node کپی کنید. فایل جدیدی را با نام server-dotnet-query.js در پروژه Node ایجاد کنید و کد زیر را در آن وارد کنید:
var http = require('http');
var edge = require('edge');
var port = process.env.PORT || 8080;

// Set up the assembly to call from Node.js 
var querySample = edge.func({ assemblyFile: 'EdgeSampleLibrary.dll', typeName: 'EdgeSampleLibrary.Sample1', methodName: 'Invoke' });

function logError(err, res) { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write("Got error: " + err); res.end(""); }

http.createServer(function (req, res) { res.writeHead(200, { 'Content-Type': 'text/html' });

    // This is the data we will pass to .NET
    var data = { pageNumber: 1, pageSize: 3 };

    // Invoke the .NET function
    querySample(data, function (error, result) {
        if (error) { logError(error, res); return; }
        if (result) {
            res.write("<ul>");
            result.forEach(function(user) {
                res.write("<li>" + user.FirstName + " " + user.LastName + ": " + user.Email + "</li>");
            });
            res.end("</ul>");
        }
        else {
            res.end("No results");
        }
    });

}).listen(port);

console.log("Node server listening on port " + port);
سپس از طریق command prompt آن را اجرا کنید:
node server-dotnet-query.js
حال مرورگر خود را باز کرده و به آدرس http://localhost:8080 بروید. در صورتیکه همه چیز به درستی انجام گرفته باشد، لیستی از 3 کاربر را خواهید دید. مقادیر pageNumber و pageSize را در فایل جاوااسکریپت تغییر دهید و تاثیر آن را بر روی خروجی مشاهده کنید.
 
نکته: برای ایجاد pageNumber و pageSize داینامیک با استفاده از ارسال مقادیر توسط QueryString، می‌توانید از ماژول connect استفاده کنید.
مطالب
آموزش زمانبندی کارها با 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));
با اجرای این متد، اول کاری برای تکرار در زمانبندی ماهیانه ایجاد میشود و در متد دوم، زمانی برای حذف متد اول مشخص میکند.

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

مطالب
استفاده از Workflow برای تغییر سطح دسترسی به یک سند پس از تایید آن
در این پست قصد دارم نحوه تغییر دسترسی یک سند را بیان کنم . مسئله : فرض کنید قرار است سندی در کتابخانه بارگذاری شود و پس از تغییرات و پالایش توسط فردی تایید (Approve) شود و پس از تایید ، دیگر تهیه کننده اولیه سند نمی‌تواند آن را ویرایش کند . با استفاده از workflow این روند را با هم پیش می‌گیریم .

یک کتابخانه به نام FinalApprovedLib در شیرپوینت ساخته و در شیرپوینت دیزاینر ان را انتخاب می‌کنیم : 

سپس وارد تنظیمات کتابخانه شده و روی List Workflow کلیک می‌کنیم : 

در پنجره نامی را برای چرخه کاری انتخاب می‌کنیم :  

درگام اول چرخه کاری ، یک Start Approval Process انتخاب می‌کنیم (از نوار action ریبون بالای پنجره قابل مشاهده است ؛ همچنین می‌توانید چند تا از حروف این گام را تایپ کنید تا به صورت خودکار لود شود) : 

سپس روی these users کلیک کرده تا تایید کننده را مشخص کنیم :

در این مرحله پنجره زیر باز شده و عنوان پیغام و محتوای آن را می‌نویسیم : 

سپس در بخش participants نام فرد ، گروه یا افرادی که می‌توانند این چرخه را تایید کنند را مشخص میکنیم :

 

در اینجا کاربری با نام usr1 باید بتواند این سند را تایید کند : 

روی add و سپس روی OK کلیک می‌کنیم : 

اگر در همین مرحله چرخه را publish کنیم می‌توانیم آن را در لیست چرخه‌های کاری لیست مشاهده کنیم :

 

در گام بعدی نیاز به یک impersonation Step داریم (اطلاعات بیشتر درباره این گام در اینجا و اینجا ) :

در ابتدا یک If current item field equals value انتخاب می‌کنیم و مانند زیر آن را تنظیم می‌کنیم :
 

سپس واژه equal که پیش فرض است و سپس عبارت سمت راست تساوی که وضعیت چرخه است :

 

حال می‌خواهیم زمانی که سند تایید شده (شرط true بود) ، عملیات تغییر دسترسی اعمال شود :

 

گزینه replace list item permission را انتخاب می‌کنیم و مانند زیر آن را تنظیم می‌کنیم :

 باید permission مورد نظر را اتتخاب کنیم و تعریف کنیم که این دسترسی برای چه کسی و به چه نوعی تغییر کند :
ابتدا فردی که باید دسترسی او تغییر کند (در اینجا سازنده سند جاری ) : 

سپس دسترسی جدید او : 

و تایید تنظیمات اعمال شده (می توانید تغییرات را در گام تعریف شده مشاهده کنید) :

 

حال چرخه کاری را ذخیره و publish می‌کنیم:

 

حال اگر workflow‌های کتابخانه را بررسی کنیم ، می‌توانیم ذخیره شدن این چرخه را در کتابخانه ببینیم :

 

فقط نکته ای که نباید فراموش شود ، نحوه start شدن workflow است که باید ان را نیز تنظیم کنیم :

در همان شیرپوینت دیزاینر روی چرخه کاری کلیک راست می‌کنیم و در تنظیمات چرخه کاری ، مانند زیر عمل می‌کنیم تا به محض افزودن یا اعمال تغییرات در سند ، چرخه کاری فراخوانی شود :

 

 (بعد از این تغییرات مجدد عمل publish را انجام دهید)

حال سندی را بارگذاری می‌کنیم تا این چرخه را تست کنیم : 

با نام کاربری usr2 این سند را بارگذاری می‌کنیم :

 

اگر به Task List سایت مراجعه کنیم می‌توانیم آغاز شدن چرخه کاری و پیغام فرستاده شده آن را ببینیم :

 

حال اگر کاربر usr1 که در بالا عملیات تایید به او انتساب داده شد ، آیتم Task list خود را باز کند ، پنجره زیر را خواهد دید :

 

و پس از تایید او دکمه ویرایش برای usr2 (که سند را ایجاد کرده بود) غیر فعال می‌شود :

- نکته اینکه در صورتی که usr2 مجدد سندی را با همان نوع و نام بارگذاری کند ، سیستم به او پیغام می‌دهد که دسترسی برای این کار را ندارد 

موفق باشید