پشتیبانی توکار از انجام کارهای پسزمینه در ASP.NET Core 2x
نویسنده: وحید نصیری
تاریخ: ۱۳۹۷/۱۱/۳۰ ۱۹:۴۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
namespace Microsoft.Extensions.Hosting
{
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
} 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;
}
}
} namespace MvcTest
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHostedService, SampleHostedService>(); services.AddHostedService<SampleHostedService>();
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
Startup.cs(82,56): error CS0104: 'IHostingEnvironment' is an ambiguous reference between 'Microsoft.AspNetCore.Hosting.IHostingEnvironment' and 'Microsoft.Extensions.Hosting.IHostingEnvironment'
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. 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);
}
} 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 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);
} dotnet add package ncrontab
┌───────────── 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) │ │ │ │ │ │ │ │ │ │ * * * * *
* * * * * * - - - - - - | | | | | | | | | | | +--- day of week (0 - 6) (Sunday=0) | | | | +----- month (1 - 12) | | | +------- day of month (1 - 31) | | +--------- hour (0 - 23) | +----------- min (0 - 59) +------------- sec (0 - 59)
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);
} 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;
}
} namespace MvcTest
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHostedService<MyScheduledTask>(); 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 public interface IBackgroundTaskQueue : ISingletonDependency
{
void QueueBackgroundWorkItem(Func<CancellationToken, IServiceProvider, Task> workItem);
Task<Func<CancellationToken, IServiceProvider, Task>> DequeueAsync(
CancellationToken cancellationToken);
} 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;
}
} 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.");
}
} 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;
})
}
} ## 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
}
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;
}
} dotnet new worker
<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> 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>();
});
}
} 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);
}
}
} public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices((hostContext, services) =>
{
//services.AddHttpClient();
services.AddHostedService<Worker>();
}); cs create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSystemd()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
}); sc create WorkerServiceDemo binPath=C:\Path\To\WorkerServiceDemo.exe
private async Task DoTaskAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync())
{
Console.WriteLine($"Firing at {DateTime.Now}");
}
} 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");
}
} //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>();
} //"Main" method
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
//Startup logic here
host.Run();
} یک نکتهی تکمیلی: روش ثبت خودکار سرویسهای پسزمینه
اگر میخواهید با استفاده از 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;
});مزیت اینکار، شروع و همچنین پایان سریعتر برنامه، با داشتن تعداد زیادی کار پسزمینه است.
BackgroundService مشتق شدهاند، یک CancellationToken را در متد ExecuteAsync(CancellationToken stoppingToken) دریافت میکنند. این توکن که معمولاً به نام stoppingToken شناخته میشود، در یکی از دو حالت زیر لغو (Cancel) میشود و به سرویس پسزمینه اعلام میکند که باید به صورت Graceful Shutdown (خاموشی با رعایت خاتمهی عملیات جاری) متوقف شود:StopAsyncتوسط Host یا خاموشی نرم (Graceful Shutdown)Ctrl+C در کنسول، یا ارسال سیگنال توقف).StopAsync: در فرآیند خاموشی، Host متد IHostedService.StopAsync(CancellationToken cancellationToken) را روی تمام سرویسهای میزبانی شده (Hosted Services) فراخوانی میکند.BackgroundService در پیادهسازی متد StopAsync خود، stoppingToken را که به ExecuteAsync فرستاده، لغو میکند تا اجرای حلقهی کار اصلی (مانند حلقهی while (!stoppingToken.IsCancellationRequested)) متوقف شود و متد ExecuteAsync به طور کامل خارج شود.CancellationTokenمتدStartAsync یا لغو شدن در حین راهاندازیCancellationToken ارسال شده به متد StartAsync(CancellationToken cancellationToken)، لغو شود.StartAsync را اجرا میکند) قبل از تکمیل، لغو شود (مثلاً به دلیل خطای شدید در حین راهاندازی برنامه)، BackgroundService این لغو را به stoppingToken که به ExecuteAsync داده شده، پیوند (Link) میدهد. در نتیجه، حتی اگر ExecuteAsync تازه شروع به کار کرده باشد، فوراً سیگنال توقف را دریافت میکند.OperationCanceledException (OCE) در متد ExecuteAsync انجام میشود.ExecuteAsyncOperationCanceledException را در داخل متد ExecuteAsync مدیریت کنید.OperationCanceledExceptionwhile) در متد ExecuteAsync، شما باید عملیاتهای await خود را داخل یک بلوک try-catch قرار دهید تا بتوانید OCE را که هنگام لغو شدن stoppingToken پرتاب میشود، بگیرید.protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// ... تنظیمات اولیه
try
{
while (!stoppingToken.IsCancellationRequested)
{
// عملیات کاری اصلی
_logger.LogInformation("سرویس در حال انجام کار در {Time}", DateTimeOffset.Now);
// Task.Delay برای توقف و انتظار، معمولاً با توکن ارسال میشود
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
catch (OperationCanceledException ex) when (stoppingToken.IsCancellationRequested)
{
// مورد 1: خاموشی نرم (Graceful Shutdown)
// اگر استثنا به دلیل لغو توسط stoppingToken رخ داده باشد، این یک توقف عادی است.
_logger.LogWarning("✅ سرویس پسزمینه به دلیل خاموشی نرم Host متوقف شد.");
return; // خروج عادی از متد
}
catch (OperationCanceledException ex)
{
// مورد 2: لغو شدن غیرمنتظره (مثلاً لغو در StartAsync یا جای دیگر)
_logger.LogError("⚠️ سرویس به طور غیرمنتظره لغو شد: {Message}", ex.Message);
// میتوانید اینجا استثنا را دوباره پرتاب کنید تا Host متوجه شود
throw;
}
catch (Exception ex)
{
// 🚨 سایر خطاها
_logger.LogError(ex, "❌ خطای غیرمنتظره در اجرای سرویس: {Message}", ex.Message);
}
}IsCancellationRequestedبرای تفکیک حالتهاOperationCanceledException (OCE)، وضعیت stoppingToken.IsCancellationRequested را بررسی کنید:| وضعیت stoppingToken | معنی پیام | پیام خطا/لاگ مناسب |
true | خاموشی نرم (Graceful Shutdown): توقف مورد انتظار و عادی توسط Host. | ✅ سرویس به طور عادی و در پاسخ به درخواست توقف Host متوقف شد. |
false | لغو غیرمنتظره: OCE از یک عملیات داخلی پرتاب شده، یا توکن StartAsync لغو شده و Host هنوز stoppingToken را لغو نکرده است. | ⚠️ عملیات در حال اجرا به طور غیرمنتظره لغو شد، اما Host هنوز درخواست توقف نکرده است. |
LogInformation یا LogWarning واضح ثبت کنید که به مدیر سیستم میگوید: "این توقف، خطا نیست، بلکه خاموشی مورد انتظار سیستم است."LogError ثبت میشوند، واقعاً نشاندهندهی شرایط خطای جدی و نیازمند بررسی خواهند بود.