پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: یازده دقیقه

از زمان ASP.NET Core 2.1، قابلیت جدیدی به نام Generic Host، به آن اضافه شده‌است که از آن می‌توان برای انجام کارهای متداول پس زمینه، مانند ارسال ایمیل‌های خبرنامه‌ی یک برنامه، تهیه فایل‌های پشتیبان و غیره استفاده کرد.


Generic Host چیست؟

Generic Host یکی از ویژگی‌های جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای این‌کار، امکان استفاده‌ی از آن نه فقط در پروژه‌های وب، بلکه در پروژه‌های کنسول نیز می‌باشد. به این ترتیب می‌توان کارهای غیر HTTP را از برنامه‌ی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پس‌زمینه‌ی خارج از اعمال web host جاری، ارائه داده‌اند:
namespace Microsoft.Extensions.Hosting
{
    public interface IHostedService
    {
        Task StartAsync(CancellationToken cancellationToken);
        Task StopAsync(CancellationToken cancellationToken);
    }
}
بنابراین برای ایجاد یک HostedService، نیاز است سرویس کارهای پس‌زمینه‌ی ما، اینترفیس IHostedService را پیاده سازی کند. متد StartAsync آن جائی‌است که تنها یکبار پس از آغاز برنامه اجرا می‌شود و هدف آن اجرای کار پس‌زمینه‌ی مدنظر است. متد StopAsync نیز دقیقا پیش از خاتمه‌ی برنامه فراخوانی خواهد شد تا اگر نیاز به پاکسازی منابعی وجود داشته باشد، بتوان از این فرصت استفاده کرد. به این ترتیب اگر نیاز به اجرای متناوب کار پس‌زمینه‌ای وجود دارد، پیاده سازی آن به خود ما واگذار شده‌است.


یک مثال: معرفی کار پس‌زمینه‌ای که هر دو ثانیه یکبار انجام می‌شود

در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ می‌شود و اگر برنامه را توسط دستور dotnet run اجرا کنید، می‌توانید خروجی آن‌را در کنسول، مشاهده کنید:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace MvcTest
{
    public class SampleHostedService : IHostedService
    {
        private readonly ILogger<SampleHostedService> _logger;

        public SampleHostedService(ILogger<SampleHostedService> logger)
        {
            _logger = logger;
        }

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Starting Hosted service");

            while (!cancellationToken.IsCancellationRequested)
            {
                _logger.LogInformation("Hosted service executing - {0}", DateTime.Now);
                await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
            }
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Stopping Hosted service");
            return Task.CompletedTask;
        }
    }
}
در ادامه برای معرفی این کار پس‌زمینه به سیستم به صورت یک سرویس با طول عمر Singleton خواهیم داشت:
namespace MvcTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IHostedService, SampleHostedService>();
روش دیگر انجام اینکار استفاده از متد الحاقی AddHostedService است:
services.AddHostedService<SampleHostedService>();
مزیت اینکار این است که متد Configure واقع در کلاس Startup یک چنین امضایی را دارد:
 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
و IHostingEnvironment هم در فضای نام Microsoft.AspNetCore.Hosting واقع شده‌است و هم در فضای نام Microsoft.Extensions.Hosting که IHostedService در آن قرار دارد. به همین جهت چون متد AddHostedService، تعریف IHostedService را مخفی می‌کند، خطای زمان کامپایلی را جهت مشخص سازی صریح فضای نام  IHostingEnvironment دریافت نخواهید کرد:
Startup.cs(82,56): error CS0104: 'IHostingEnvironment' is an ambiguous reference between
'Microsoft.AspNetCore.Hosting.IHostingEnvironment' and 'Microsoft.Extensions.Hosting.IHostingEnvironment'


مشکلات پیاده سازی کار پس‌زمینه‌ی SampleHostedService فوق

هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
D:\MvcTest>dotnet run
info: MvcTest.SampleHostedService[0]
      Starting Hosted service
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 14:45:10
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 14:45:12
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 14:45:14
Ctrl+C
Application is shutting down...
Hosting environment: Development
Content root path: D:\MvcTest
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
پس از اجرای دستور dotnet run، سرویس پس زمینه شروع به کار کرده‌است. پس از مدتی کلیدهای Ctrl+C را فشرده‌ایم تا این حلقه‌ی بی‌نهایت و برنامه خاتمه یابد. اینجا است که مشاهده می‌کنید تازه قسمت هاست برنامه‌ی وب ما شروع به کار کرده‌است؛ یعنی دقیقا زمانیکه پروسه‌ی برنامه در حال خاتمه یافتن است. چرا اینگونه رفتار کرده‌است؟
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب می‌شود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته می‌شود و چون در پیاده سازی فوق این نکته رعایت نشده‌است، این Task همواره در حال اجرا و خاتمه نیافته محسوب می‌شود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شده‌ی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آن‌را فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمه‌ی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شده‌است:
namespace Microsoft.Extensions.Hosting
{
    public abstract class BackgroundService : IHostedService, IDisposable
    {
        protected BackgroundService();
        public virtual void Dispose();
        public virtual Task StartAsync(CancellationToken cancellationToken);
        public virtual Task StopAsync(CancellationToken cancellationToken);
        protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
    }
}
برای استفاده‌ی از آن تنها کافی است متد ExecuteAsync آن‌را پیاده سازی کنیم. به این ترتیب اینبار پیاده سازی SampleHostedService به صورت زیر تغییر می‌کند:
namespace MvcTest
{
    public class PrinterHostedService : BackgroundService
    {
        private readonly ILogger<SampleHostedService> _logger;

        public PrinterHostedService(ILogger<SampleHostedService> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Starting Hosted service");

            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Hosted service executing - {0}", DateTime.Now);
                await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
            }
        }
    }
}
اینبار اگر این کار پس‌زمینه را به سیستم معرفی:
namespace MvcTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHostedService<PrinterHostedService>();
و سپس برنامه را اجرا کنیم:
D:\MvcTest>dotnet run
Hosting environment: Development
infoContent root path: D:\MvcTest
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
: MvcTest.SampleHostedService[0]
      Starting Hosted service
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 15:00:23
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 15:00:25
info: MvcTest.SampleHostedService[0]
      Hosted service executing - 02/19/2019 15:00:27
Application is shutting down...
^C
مشاهده می‌کنیم که ابتدا هاست وب برنامه شروع به کار کرده‌است و سپس سرویس انجام کارهای پس‌زمینه در حال اجرا است و به این ترتیب اجرای این سرویس پس‌زمینه، تداخلی را در کار برنامه‌ی وب ایجاد نکرده‌است. بنابراین از این پس بجای استفاده‌ی از IHostedService خام، از نمونه‌ی بهبود یافته‌ی BackgroundService آن استفاده کنید.


یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پس‌زمینه

IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگی‌ها معرفی می‌شوند. در این حالت اگر سرویس‌هایی با طول عمر transient و یا scoped را به آن‌ها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آن‌ها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگی‌های NET Core. با صدور استثنائی، از این مساله جلوگیری می‌کند (در این مورد در مطالب «مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامه‌های وب» و همچنین «قسمت سوم - رهاسازی منابع سرویس‌های IDisposable» بیشتر بحث شده‌است). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope می‌توان مدیریت کرد:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

public abstract class ScopedBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public ScopedBackgroundService(IServiceScopeFactory serviceScopeFactory)
    {
        _serviceScopeFactory = serviceScopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            await ExecuteInScope(scope.ServiceProvider, stoppingToken);
        }
    }

    public abstract Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken);
}
از این پس برای تعریف کارهای پس‌زمینه‌ای که نیاز به تزریق سرویس‌هایی با طول عمر Scoped یا Transient دارند، می‌توان کلاس سرویس وظیفه را از ScopedBackgroundService مشتق کرد و سپس متد ExecuteInScope آن‌را پیاده سازی نمود. serviceProvider ای که در اینجا در اختیار مصرف کننده قرار می‌گیرد، داخل Scope قرار دارد و توسط آن می‌توان سرویس‌های مدنظر را توسط متدهایی مانند serviceProvider.GetRequiredService، دریافت کرد.


طراحی سرویس کارهای پس‌زمینه‌ی زمان‌بندی شده

ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمی‌کند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیل‌های خبرنامه‌ی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شده‌است. برای پیاده سازی بهتر این تایمر می‌توان از کتابخانه‌ی NCrontab که توسط نویسنده‌ی کتابخانه‌ی معروف ELMAH تهیه شده‌است، استفاده کرد که با برنامه‌های NET Core. نیز سازگاری دارد:
 dotnet add package ncrontab
عبارات Cron، روش بسیار متداولی برای تعریف و انجام کارهای زمانبندی شده در سیستم‌های لینوکسی هستند. برای مثال عبارت * * * 0 1 سبب اجرای یک وظیفه، هر روز یک دقیقه پس از نیمه‌شب، می‌شود و فرمت کلی 5 قسمتی آن، به صورت زیر است:
┌───────────── minute (0 - 59) 
│ ┌───────────── hour (0 - 23) 
│ │ ┌───────────── day of month (1 - 31) 
│ │ │ ┌───────────── month (1 - 12) 
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; 
│ │ │ │ │                                       7 is also Sunday on some systems) 
│ │ │ │ │ 
│ │ │ │ │ 
* * * * *
و یا عبارت 6 قسمتی آن چنین مفهومی را دارد:
* * * * * *
- - - - - -
| | | | | |
| | | | | +--- day of week (0 - 6) (Sunday=0)
| | | | +----- month (1 - 12)
| | | +------- day of month (1 - 31)
| | +--------- hour (0 - 23)
| +----------- min (0 - 59)
+------------- sec (0 - 59)
اگر ScopedBackgroundService فوق را با CrontabSchedule یاد شده ترکیب کنیم، می‌توانیم به یک کلاس abstract دیگر برسیم که طراحی کلاس پایه‌ی اجرای کارهای زمانبندی شده را ارائه می‌دهد:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NCrontab;
using static NCrontab.CrontabSchedule;

public abstract class ScheduledScopedBackgroundService : ScopedBackgroundService
{
    private CrontabSchedule _schedule;
    private DateTime _nextRun;

    protected abstract string Schedule { get; }

    public ScheduledScopedBackgroundService(IServiceScopeFactory serviceScopeFactory)
     : base(serviceScopeFactory)
    {
        _schedule = CrontabSchedule.Parse(Schedule, new ParseOptions { IncludingSeconds = true });
        _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
    }

    public override async Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    {
        do
        {
            var now = DateTime.Now;
            if (now > _nextRun)
            {
                await ScheduledExecuteInScope(serviceProvider, stoppingToken);
                _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
            }
            await Task.Delay(1000, stoppingToken); //1 second delay
        }
        while (!stoppingToken.IsCancellationRequested);
    }

    public abstract Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken);
}
این کلاس پایه، توسط متد CrontabSchedule.Parse، مقدار رشته‌ای Schedule را با فرمت Cron (فرمت 6 قسمتی که دارای ثانیه هم هست) دریافت و پردازش می‌کند. سپس متد GetNextOccurrence، زمان بعدی اجرای این وظیفه را مشخص می‌کند.
روش استفاده‌ی از آن برای تعریف یک وظیفه‌ی جدید نیز به صورت زیر است:
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class MyScheduledTask : ScheduledScopedBackgroundService
{
    private readonly ILogger<MyScheduledTask> _logger;

    public MyScheduledTask(
        IServiceScopeFactory serviceScopeFactory,
        ILogger<MyScheduledTask> logger) : base(serviceScopeFactory)
    {
        _logger = logger;
    }

    protected override string Schedule => "*/10 * * * * *"; //Runs every 10 seconds

    public override Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken)
    {
        _logger.LogInformation("MyScheduledTask executing - {0}", DateTime.Now);
        return Task.CompletedTask;
    }
}
در اینجا ابتدا کار با پیاده سازی کلاس پایه ScheduledScopedBackgroundService شروع می‌شود. سپس باید مقدار Schedule را با فرمت 6 قسمتی مشخص کرد. برای مثال در سرویس فوق، این تنظیم سبب اجرای هر 10 ثانیه یکبار این وظیفه می‌گردد. در آخر، خود وظیفه داخل متد ScheduledExecuteInScope تعریف خواهد شد که serviceProvider دریافتی آن، داخل یک Scope قرار دارد.
روش معرفی آن به سیستم نیز مانند قبل است:
namespace MvcTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHostedService<MyScheduledTask>();
در این حالت اگر برنامه را اجرا کنید، یک چنین خروجی را که بیانگر اجرای هر 10 ثانیه یکبار وظیفه‌ی تعریف شده‌است، مشاهده می‌کنید:
D:\MvcTest>dotnet run
Hosting environment: Development
Content root path: D:\MvcTest
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
info: MyScheduledTask[0]
      MyScheduledTask executing - 02/19/2019 19:18:50
info: MyScheduledTask[0]
      MyScheduledTask executing - 02/19/2019 19:19:00
info: MyScheduledTask[0]
      MyScheduledTask executing - 02/19/2019 19:19:10
Application is shutting down...
^C
  • #
    ‫۵ سال و ۶ ماه قبل، چهارشنبه ۱ اسفند ۱۳۹۷، ساعت ۰۰:۲۹
    نکته تکمیلی: معادل  HostingEnvironment.QueueBackgroundWorkItem  در ASP.NET Core
    public interface IBackgroundTaskQueue : ISingletonDependency
    {
        void QueueBackgroundWorkItem(Func<CancellationToken, IServiceProvider, Task> workItem);
    
        Task<Func<CancellationToken, IServiceProvider, Task>> DequeueAsync(
            CancellationToken cancellationToken);
    }
    با تزریق این IBackgroundTaskQueue و استفاده از متد QueueBackgoundWorkItem، امکان در صف قرار دادن یک وظیفه جدید را خواهید داشت. 
    پیاده سازی واسط IBackgroundTaskQueue
    internal class BackgroundTaskQueue : IBackgroundTaskQueue
    {
        private readonly ConcurrentQueue<Func<CancellationToken, IServiceProvider, Task>> _workItems =
            new ConcurrentQueue<Func<CancellationToken, IServiceProvider, Task>>();
    
        private readonly SemaphoreSlim _signal = new SemaphoreSlim(0);
    
        public void QueueBackgroundWorkItem(
            Func<CancellationToken, IServiceProvider, Task> workItem)
        {
            if (workItem == null)
            {
                throw new ArgumentNullException(nameof(workItem));
            }
    
            _workItems.Enqueue(workItem);
            _signal.Release();
        }
    
        public async Task<Func<CancellationToken, IServiceProvider, Task>> DequeueAsync(
            CancellationToken cancellationToken)
        {
            await _signal.WaitAsync(cancellationToken);
            _workItems.TryDequeue(out var workItem);
    
            return workItem;
        }
    }
    در زمان ثبت و معرفی یک کار پس‌زمینه، داخل صفی با رعایت مباحث همزمانی و تحت عنوان ‎_workItems قرار خواهد گرفت. متد DequeueAsync نیز توسط HostedService پیاده سازی شده در ادامه، استفاده شده و به ترتیب وظایف ثبت شده را اجرا خواهد کرد.
    پیاده سازی یک QueuedHostedService 
    public class QueuedHostedService : BackgroundService
    {
        private readonly IServiceScopeFactory _factory;
        private readonly ILogger _logger;
        private readonly IBackgroundTaskQueue _queue;
    
        public QueuedHostedService(
            IBackgroundTaskQueue queue,
            IServiceScopeFactory factory,
            ILoggerFactory loggerFactory)
        {
            _factory = factory ?? throw new ArgumentNullException(nameof(factory));
            _queue = queue ?? throw new ArgumentNullException(nameof(queue));
            _logger = loggerFactory.CreateLogger<QueuedHostedService>();
        }
    
    
        protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    
        {
            _logger.LogInformation("Queued Hosted Service is starting.");
    
            while (!cancellationToken.IsCancellationRequested)
            {
                var workItem = await _queue.DequeueAsync(cancellationToken);
    
                try
                {
                    using (var scope = _factory.CreateScope())
                    {
                        await workItem(cancellationToken, scope.ServiceProvider);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex,
                        $"Error occurred executing {nameof(workItem)}.");
                }
            }
    
            _logger.LogInformation("Queued Hosted Service is stopping.");
        }
    }
    این امکان قرار است به صورت آزمایشی به نسخه ASP.NET Core 3.0 اضافه شود. برای استفاده از آن کافی است QueuedHostedService را به سیستم DI معرفی کرده به شکل زیر عمل کنید:
    public class InvoiceService : IInvoiceService
    {
       private readonly IBackgroundTaskQueue _queue;
       
       public InvoiceService(IBackgroundTaskQueue queue)
       {
         _queue = queue ?? throw new ArgumentNullException(nameof(queue));
       }
       
       public Print(InvoiceModel model)
       {
          _queue.QueueBackgroundWorkItem((token, provider)=>
          {
          //todo: print
          return Task.Task.CompletedTask;
          })
       }
    }

  • #
    ‫۵ سال و ۶ ماه قبل، چهارشنبه ۱ اسفند ۱۳۹۷، ساعت ۱۷:۰۲
    ضمن تشکر؛ در صورت استفاده از روش فوق آیا پیاده سازی روشی برای زنده نگه داشتن برنامه ضرورت دارد و توصیه می‌شود؟ همان نکته ای که تحت عنوان PingTask در کتابخانه‌ی DNTScheduler.Core وجود دارد. 
    • #
      ‫۵ سال و ۶ ماه قبل، چهارشنبه ۱ اسفند ۱۳۹۷، ساعت ۱۷:۲۱
      از این لحاظ تفاوتی نمی‌کند.
    • #
      ‫۵ سال و ۵ ماه قبل، سه‌شنبه ۲۸ اسفند ۱۳۹۷، ساعت ۱۸:۰۷
      یک نکته‌ی تکمیلی: الزامات زنده نگه داشتن کارهای پس‌زمینه در IIS به صورت یک اسکریپت پاورشل
          ## IIS WebAdmin Module
          Import-Module WebAdministration
      
          $AppPoolInstance = Get-Item IIS:\AppPools\$AppPool
      
          Write-Output "Set Site PreLoadEnabled to true"
          Set-ItemProperty IIS:\Sites\$Site -name applicationDefaults.preloadEnabled -value True
      
          Write-Output "Set Recycling.periodicRestart.time  = 0"
          $AppPoolInstance.Recycling.periodicRestart.time = [TimeSpan]::Parse("0");
          $AppPoolInstance | Set-Item
      
          Write-Output "Set App Pool start up mode to AlwaysRunning"
          $AppPoolInstance.startMode = "alwaysrunning"
      
          Write-Output "Disable App Pool Idle Timeout"
          $AppPoolInstance.processModel.idleTimeout = [TimeSpan]::FromMinutes(0)
          $AppPoolInstance | Set-Item
      
          if ($appPoolStatus -ne "Started") {
              Write-Output "Starting App Pool"
              Start-WebAppPool $AppPool    
          } else {
              Write-Output "Restarting App Pool"
              Restart-WebAppPool $AppPool
          }
  • #
    ‫۵ سال و ۶ ماه قبل، شنبه ۱۸ اسفند ۱۳۹۷، ساعت ۱۲:۳۴
    یک نکته‌ی تکمیلی: به مجموعه قالب‌های NET Core 3.0.، قالب Worker Service هم اضافه شده‌است که در اصل یک public class Worker : BackgroundService را هاست می‌کند:

  • #
    ‫۵ سال و ۱ ماه قبل، شنبه ۵ مرداد ۱۳۹۸، ساعت ۱۹:۳۴
    با این روش یک تسک در بک گراند همیشه در حال اجرا هست . آیا میتوان کاری کرد که بتوانیم تسکی را در قسمت مدیریت در هر زمان مورد نیاز ، فعال و غیرفعال کنیم ؟ممنون
    • #
      ‫۵ سال و ۱ ماه قبل، شنبه ۵ مرداد ۱۳۹۸، ساعت ۲۰:۰۵
      - یک وظیفه در اینجا همیشه در حال اجرا نیست. فقط زمانیکه به تنظیمات خاصیت Schedule آن برسد، اجرا می‌شود. تمام وظایف پشت صحنه به همین صورت اجرا و مدیریت می‌شوند. یک حلقه مخصوص بررسی رسیدن به زمان‌بندی مدنظر وجود دارد و سپس اجرای آن وظیفه‌ی خاص. نمونه‌ی دیگر آن پروژه‌ی « DNTScheduler.Core » است که معادل NET Core. مطلب «انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler» هست. 
      - برای غیرفعال کردن یک Task در مطلب جاری، باید آن‌را از لیست سرویس‌های ثبت شده‌ی سیستم حذف کنید (و یا برای معرفی آن به سیستم باید به سیستم تزریق وابستگی‌ها توسط services.AddHostedService اضافه شود).
      public static class ServiceCollectionExtensions
      {
          public static IServiceCollection Remove<T>(this IServiceCollection services)
          {
              var serviceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(T));
              if (serviceDescriptor != null) services.Remove(serviceDescriptor);
      
              return services;
          }
      }
      و یا با توجه به اینکه این وظایف به صورت یک سرویس ثبت می‌شوند، می‌توانید یک سرویس سفارشی فعال یا غیرفعالسازی را تعریف کنید و آن‌را به سازنده‌ی این وظایف تزریق و استفاده کنید. برای مثال زمانیکه حلقه‌ی انجام وظایف به به تنظیمات خاصیت Schedule رسید، متد ScheduledExecuteInScope را اجرا می‌کند. در این متد فرصت خواهید داشت تا سرویس سفارشی جدید تزریق شده را بررسی کرده و از فعال بودن یا نبودن این وظیفه مطلع شوید (برای مثال این سرویس بر اساس نامی که به آن ارسال می‌کنید، به بانک اطلاعاتی مراجعه کرده و روشن و یا خاموش بودن آن‌را بررسی کند. تنظیم بانک اطلاعاتی آن‌را هم واگذار کنید به قسمت مدیریتی برنامه).
  • #
    ‫۵ سال قبل، پنجشنبه ۳۱ مرداد ۱۳۹۸، ساعت ۱۶:۵۶
    ارتقاء به NET Core 3.0.: پشتیبانی از ایجاد سرویس‌های پس‌زمینه

    یکی از تغییرات مهم قالب ایجاد پروژه‌های ASP.NET Core 3.0، تغییر فایل program.cs آن است که در آن از یک Generic Host بجای روش قبلی Web Host، استفاده شده‌است. علت آن فراهم آوردن امکان استفاده‌ی از قابلیت‌هایی مانند تزریق وابستگی‌ها، logging، تنظیمات برنامه و غیره، در برنامه‌های غیر وب نیز می‌باشد. یکی از این انواع برنامه‌ها، سرویس‌های پس‌زمینه‌ی غیر HTTP هستند. به این ترتیب می‌توان برنامه‌ای شبیه به یک برنامه‌ی وب ASP.NET Core را ایجاد کرد که تنها کارش اجرای سرویس‌های غیر وبی است؛ اما به تمام امکانات و زیر ساخت‌های ASP.NET Core دسترسی دارد.
    برای ایجاد این نوع برنامه‌ها در NET Core 3x. می‌توانید دستور زیر را در پوشه‌ی خالی که ایجاد کرده‌اید، اجرا کنید:
    dotnet new worker
    ساختار برنامه‌ای که توسط این دستور تولید می‌شود به صورت زیر است که بسیار شبیه به ساختار یک برنامه‌ی ASP.NET Core است:
    appsettings.Development.json
    appsettings.json
    MyWorkerServiceApp.csproj
    Program.cs
    Worker.cs

    - فایل csproj آن دارای این محتوا است:
    <Project Sdk="Microsoft.NET.Sdk.Worker">
      <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
        <UserSecretsId>dotnet-MyWorkerServiceApp-B76DB08E-FFBB-4AD1-89B5-93BF483D1BD0</UserSecretsId>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="3.0.0-preview8.19405.4" />
      </ItemGroup>
    </Project>
    در آن ویژگی Sdk به Microsoft.NET.Sdk.Worker اشاره می‌کند و همچنین از بسته‌ی Microsoft.Extensions.Hosting استفاده شده‌است.

    - محتوای فایل Program.cs آن بسیار آشنا است و دقیقا کپی همان فایلی است که در برنامه‌های ASP.NET Core 3x حضور دارد:
    namespace MyWorkerServiceApp
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                CreateHostBuilder(args).Build().Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureServices((hostContext, services) =>
                    {
                        services.AddHostedService<Worker>();
                    });
        }
    }
    در اینجا یک Generic host را بجای Web host قالب‌های پیشین فایل Program.cs ملاحظه می‌کنید که هدف اصلی آن، عمومی کردن این قالب، برای استفاده‌ی از آن در برنامه‌های غیر وبی نیز می‌باشد.
    در متد ConfigureServices، انواع اقسام سرویس‌ها را منجمله یک HostedService که در مطلب جاری به آن پرداخته شده، می‌توان افزود. سرویس Worker ای که در اینجا به آن ارجاعی وجود دارد، به صورت زیر تعریف شده‌است:
        public class Worker : BackgroundService
        {
            private readonly ILogger<Worker> _logger;
    
            public Worker(ILogger<Worker> logger)
            {
                _logger = logger;
            }
    
            protected override async Task ExecuteAsync(CancellationToken stoppingToken)
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
                    await Task.Delay(1000, stoppingToken);
                }
            }
        }
    با ساختار این کلاس نیز آشنا هستید و موضوع اصلی مطلب جاری است.


    یک نکته‌ی تکمیلی: روش تبدیل کردن یک BackgroundService به یک Windows Service

    اگر برنامه‌ی NET Core. شما در ویندوز اجرا می‌شود، می‌توانید این برنامه‌ی BackgroundService را به یک سرویس ویندوز NT نیز تبدیل کنید. برای اینکار ابتدا بسته‌ی نیوگت Microsoft.Extensions.Hosting.WindowsServices را به پروژه اضافه کنید. سپس جائیکه CreateHostBuilder صورت می‌گیرد، متد UseWindowsService را فراخوانی کنید:
    public static IHostBuilder CreateHostBuilder(string[] args) => 
                Host.CreateDefaultBuilder(args) 
                    .UseWindowsService() 
                    .ConfigureServices((hostContext, services) => 
                    { 
                       //services.AddHttpClient(); 
                       services.AddHostedService<Worker>(); 
                    });
    تا اینجا هنوز هم برنامه، شبیه به یک برنامه‌ی کنسول دات نت Core قابل اجرا و دیباگ است. اما اگر خواستید آن‌را به صورت یک سرویس ویندوز نیز نصب کنید، تنها کافی است از دستور زیر استفاده کنید:
     cs create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe

    البته برای لینوکس نیز می‌توان از UseSystemd استفاده کرد که نیاز به نصب بسته‌ی Microsoft.Extensions.Hosting.Systemd را دارد:
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSystemd()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Worker>();
            });
    • #
      ‫۳ سال و ۹ ماه قبل، یکشنبه ۱۸ آبان ۱۳۹۹، ساعت ۱۵:۲۸
      با سلام؛ دستور ایجاد وب سرویس ویندوزی فکر میکنم درستش با sc شروع بشه؛ یعنی به این شکل :
       sc create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe
  • #
    ‫۵ سال قبل، چهارشنبه ۱۳ شهریور ۱۳۹۸، ساعت ۰۰:۴۰
    چطوری "*/10 * * * * *"  تبدیل شد به هر 10 ثانیه؟ هر طوری اومدم طبق دو تا الگوی بالایی مقایسه کنم جور در نیومد.
    • #
      ‫۵ سال قبل، چهارشنبه ۱۳ شهریور ۱۳۹۸، ساعت ۰۱:۰۰
      البته به این صورت نوشته شده:
      protected override string Schedule => "*/10 * * * * *"; //Runs every 10 seconds
      و تناظر دارد با نمونه‌ی 6 قسمتی. 10/* هم هر 10 ثانیه خوانده می‌شود (البته بسته به محل قرارگیری که اینجا ثانیه است):
      every x hours/min/sec = */x
      • #
        ‫۵ سال قبل، چهارشنبه ۱۳ شهریور ۱۳۹۸، ساعت ۰۲:۱۲
        خیلی تلاش کردم که معادل هر شب ساعت 2 بامدادش رو درست کنم ولی نشد. لطفا راهنمایی کنید . ممنون
          • #
            ‫۵ سال قبل، چهارشنبه ۱۳ شهریور ۱۳۹۸، ساعت ۱۴:۰۰
            کتابخانه ای برای تولید عبارات Cron  که برای تاریخ شمسی نوشته ام. کتابخانه هایی که برای تاریخ میلادی نوشته شده اند، برای تاریخ شمسی در روزهای خاصی از سال مقدار درستی را خروجی نمی‌دهند. 
  • #
    ‫۲ سال و ۶ ماه قبل، شنبه ۳۰ بهمن ۱۴۰۰، ساعت ۲۰:۲۷
    یک نکته‌ی تکمیلی: دات نت 6 و معرفی یک تایمر Async جدید

    در این مطلب برای انجام کارهای پس زمینه‌ای متناوب و async، مجبور به اختراع تایمرهای خاصی شدیم که در دات نت 6، روش بهتری برای انجام آن ارائه شده‌است. تا پیش از دات نت 6، تایمرهای زیر در فضاهای نام مختلفی تعریف شده‌اند:

    - System.Threading.Timer
    - System.Timers.Timer
    - System.Windows.Forms.Timer
    - System.Web.UI.Timer
    - System.Windows.Threading.DispatcherTimer

    طراحی تمام این تایمرها مبتنی بر callbackها است و رخ‌دادهایی که توسط تایمر، در زمان مشخصی صادر می‌شوند. این تایمرها مشکلات زیر را به همراه دارند:
    1- متد callback فراخوانی شده async نیست (زمانی طراحی شده بودند که نوع Task، هنوز وجود خارجی نداشت).
    2- اگر درون callback خطایی رخ‌دهد، خاموش سازی تایمر نیاز به عملیات اضافه‌تری دارد.
    3- اگر عملیات درون یک callback هنوز به پایان نرسیده باشد، ممکن است این callback مجددا فراخوانی شود.

    برای رفع تمام این مشکلات، تایمر جدیدی به نام PeriodicTimer به دات نت 6 اضافه شده‌است که این مزایا را به همراه دارد:
    1- تمام async است.
    2- تنها یک جریان کاری مشخص را دارد که با فراخوانی دستی متد WaitForNextTickAsync آن، به میزان بازه‌ی زمانی مشخص شده، صبر خواهد شد. وجود تنها یک جریان کاری، مشکلات 2 و 3 تایمرهای قبلی را رفع می‌کند.

    یک مثال:
    private async Task DoTaskAsync()
    {
       using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));   
       while (await timer.WaitForNextTickAsync())
       {
          Console.WriteLine($"Firing at {DateTime.Now}");
       }   
    }
    این تایمر async، هر 5 ثانیه یکبار، کدهای بدنه‌ی حلقه را اجرا می‌کند.

    اگر خواستیم این تایمر، پس از 20 ثانیه به طور کامل متوقف شود، روش کار به صورت زیر است که توسط یک CancellationTokenSource که به عنوان پارامتر متد WaitForNextTickAsync ارسال می‌شود، قابل پیاده سازی است:
    private async Task DoTaskAsync()
    {
       try
       {
          var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
    
          using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));   
          while (await timer.WaitForNextTickAsync(cts.Token))
          {
             Console.WriteLine($"Firing at {DateTime.Now}");
          }   
       }
       catch (OperationCanceledException)
       {
           Console.WriteLine("Operation cancelled");
       }
    }
    این خاتمه‌ی خودکار، با صدور یک OperationCancelled exception رخ خواهد داد و یا حتی می‌توان متد ()cts.Cancel را نیز در صورت نیاز به صورت دستی در داخل حلقه فراخوانی کرد تا عملیات خاتمه یابد.
    • #
      ‫۲ سال قبل، سه‌شنبه ۱۵ شهریور ۱۴۰۱، ساعت ۱۴:۴۵
      سلام. یعنی میتوان بجای BackgroundService  از  PeriodicTimer  استفاده نمود؟
      • #
        ‫۲ سال قبل، سه‌شنبه ۱۵ شهریور ۱۴۰۱، ساعت ۱۴:۴۹
        یعنی می‌توان از هر دوی آن‌ها با هم استفاده کرد. هیچکدام جایگزین دیگری نیستند؛ مکمل هم هستند.
  • #
    ‫۲ سال قبل، سه‌شنبه ۲۵ مرداد ۱۴۰۱، ساعت ۱۸:۳۴
    یک نکته‌ی تکمیلی: روش اجرای خودکار کدها در ابتدای کار برنامه

    حتی اگر نخواهیم از IHostedService‌ها استفاده کنیم، می‌توان از یک قابلیت جالب آن‌ها استفاده کرد: اجرای خودکار کدها در زمان آغاز برنامه.
    //Define your hosted service with startup logic
    public class MyHostedService : IHostedService
    {
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            //Startup logic here
        }
    
        public async Task StopAsync(CancellationToken cancellationToken)
        {
            //Cleanup logic here
        }
    }
    
    //Register hosted service 
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHostedService<MyHostedService>();
    }
    برای مثال بجای اینکه سرویسی را مستقیما در انتهای public static void Main فراخوانی کنیم:
    //"Main" method
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
        //Startup logic here
        host.Run();
    }
     می‌توان اجرای خودکار آن‌را به متد StartAsync فوق منتقل کرد. این روش خصوصا جهت ساده سازی توزیع کتابخانه‌ها مفید است؛ چون تنظیمات کمتری را به همراه خواهد داشت.
  • #
    ‫۱۷ روز قبل، جمعه ۹ شهریور ۱۴۰۳، ساعت ۱۴:۵۲

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

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

    services.Scan(scan => scan.FromAssembliesOf(typeof(Program))
            .AddClasses(classes => classes.AssignableTo<BackgroundService>())
            .As<IHostedService>()
            .WithSingletonLifetime());
  • #
    ‫۴ روز قبل، جمعه ۲۳ شهریور ۱۴۰۳، ساعت ۰۸:۰۲

    یک نکته‌ی تکمیلی: روش اجرای پیش‌فرض کارهای پس زمینه، ترتیبی است.

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

    builder.Services.Configure<HostOptions>(options =>
    {
        options.ServicesStartConcurrently = true;
        options.ServicesStopConcurrently = true;
    });

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