پیادهسازی سرویسهای پسزمینه (Background Services) در ASP.NET Core: راهنمای جامع
مبانی سرویسهای پسزمینه: IHostedService و BackgroundService
هستهی اصلی اجرای کارهای پسزمینه در ASP.NET Core، اینترفیس IHostedService است. هر کلاسی که این اینترفیس را پیادهسازی کند، توسط "میزبان" (Host) برنامه در زمان شروع، مدیریت شده و در زمان توقف، به آن اطلاع داده میشود.
اینترفیس IHostedService
این اینترفیس دو متد اصلی را تعریف میکند:
-
StartAsync(CancellationToken cancellationToken): این متد در زمان شروع به کار برنامه فراخوانی میشود و نقطهی آغازی برای اجرای منطق سرویس پسزمینه است.
-
StopAsync(CancellationToken cancellationToken): این متد زمانی که برنامه در حال خاموش شدن است (Graceful Shutdown)، فراخوانی میشود و به شما فرصت میدهد تا عملیات در حال اجرا را متوقف کرده و منابع را آزاد کنید.
استفادهی مستقیم از IHostedService کنترل کاملی بر روی چرخهی حیات سرویس به شما میدهد، اما پیادهسازی آن برای کارهای طولانیمدت (Long-running) میتواند کمی پیچیده باشد.
کلاس انتزاعی BackgroundService
برای سادهسازی فرآیند، ASP.NET Core یک کلاس انتزاعی به نام BackgroundService ارائه میدهد که خود، IHostedService را پیادهسازی میکند. این کلاس، پیچیدگیهای مربوط به مدیریت شروع و پایان تسک را پنهان کرده و تنها یک متد انتزاعی به نام ExecuteAsync را برای پیادهسازی در اختیار شما قرار میدهد.
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
منطق اصلی و طولانیمدت سرویس شما درون این متد قرار میگیرد. این متد یک بار فراخوانی شده و تا زمانی که برنامه در حال اجراست و یا توکن توقف (stoppingToken) فعال نشده، به کار خود ادامه میدهد. BackgroundService گزینهی پیشنهادی و رایجتر برای اکثر سناریوهای کارهای پسزمینه است.
تزریق وابستگی و مدیریت طول عمر سرویسها
یکی از چالشهای کلیدی در کار با سرویسهای پسزمینه، مدیریت صحیح تزریق وابستگی (Dependency Injection) است. سرویسهای میزبانی شده (که از IHostedService مشتق میشوند) به صورت Singleton در DI Container ثبت میشوند، به این معنی که تنها یک نمونه از آنها در طول عمر برنامه ساخته میشود.
این موضوع زمانی مشکلساز میشود که شما نیاز به استفاده از سرویسهایی با طول عمر کوتاهتر، مانند Scoped، در سرویس پسزمینه خود دارید. یک مثال بسیار رایج، استفاده از DbContext از Entity Framework Core است که به صورت Scoped ثبت میشود. تزریق مستقیم یک سرویس Scoped به یک سرویس Singleton منجر به خطای "Cannot resolve scoped service from root provider" میشود.
راه حل: برای حل این مشکل، باید به صورت دستی یک Scope جدید درون متد ExecuteAsync ایجاد کنید. این کار با تزریق IServiceProvider و استفاده از متد CreateScope انجام میشود.
public class MyBackgroundService : BackgroundService
{
private readonly ILogger<MyBackgroundService> _logger;
private readonly IServiceProvider _serviceProvider;
public MyBackgroundService(ILogger<MyBackgroundService> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = _serviceProvider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
// انجام عملیات با dbContext
_logger.LogInformation("سرویس پسزمینه در حال اجرا است.");
}
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
با این الگو، در هر بار اجرای حلقه، یک Scope جدید ایجاد شده و سرویسهای Scoped (مانند MyDbContext) به درستی نمونهسازی و پس از اتمام کار، Dispose میشوند.
خاتمهی صحیح (Graceful Shutdown) و مدیریت خطا
استفاده از CancellationToken
وقتی کاربر برنامه را متوقف میکند (مثلاً با فشردن Ctrl+C) یا میزبان (مانند IIS) دستور توقف را صادر میکند، ASP.NET Core سعی میکند برنامه را به آرامی خاموش کند. توکن stoppingToken که به متد ExecuteAsync پاس داده میشود، در این زمان فعال (Canceled) میشود. کد شما باید به این توکن حساس باشد تا عملیات جاری را لغو کرده و از حلقه خارج شود. این کار از از دست رفتن دادهها و نیمهکاره ماندن فرآیندها جلوگیری میکند.
مدیریت استثناها (Exception Handling)
اگر یک استثنا (Exception) کنترلنشده از متد ExecuteAsync خارج شود، کل برنامه میزبان (Host) ممکن است از کار بیفتد. بنابراین، بسیار مهم است که منطق اصلی خود را درون یک بلوک try-catch قرار دهید و تمام استثناهای احتمالی را مدیریت و لاگ کنید.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// منطق اصلی سرویس
}
catch (Exception ex)
{
_logger.LogError(ex, "خطایی در سرویس پسزمینه رخ داد.");
}
await Task.Delay(5000, stoppingToken);
}
}

الگوهای پیادهسازی پیشرفته
۱. کارهای پسزمینه زمانبندی شده (Timed Services)
برای اجرای یک کار در فواصل زمانی معین، میتوانید از Task.Delay در یک حلقه while استفاده کنید. اما برای کنترل دقیقتر، به خصوص در .NET 6 و بالاتر، استفاده از PeriodicTimer گزینهی بهتری است.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
_logger.LogInformation("اجرای تسک زمانبندی شده.");
// منطق تسک
}
catch (Exception ex)
{
_logger.LogError(ex, "خطا در تسک زمانبندی شده.");
}
}
}
۲. کارهای پسزمینه در صف (Queued Background Tasks)
یک الگوی بسیار متداول، ایجاد یک صف (Queue) برای کارهایی است که از بخشهای مختلف برنامه (مثلاً از Controller ها) به آن اضافه میشوند و یک سرویس پسزمینهی واحد (Consumer) مسئولیت پردازش آنها را بر عهده دارد. این الگو برای کارهایی مانند ارسال ایمیل یا نوتیفیکیشن ایدهآل است.
برای پیادهسازی این الگو میتوان از System.Threading.Channels.Channel<T> استفاده کرد که یک ساختار دادهی Thread-safe برای سناریوهای تولیدکننده/مصرفکننده (Producer/Consumer) است.
گام ۱: تعریف صف به عنوان یک سرویس Singleton
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, ValueTask>> _queue;
public BackgroundTaskQueue(int capacity)
{
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
}
public async ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem)
{
if (workItem == null) throw new ArgumentNullException(nameof(workItem));
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken)
{
var workItem = await _queue.Reader.ReadAsync(cancellationToken);
return workItem;
}
}
گام ۲: ثبت صف در Program.cs
builder.Services.AddSingleton(new BackgroundTaskQueue(100));
گام ۳: ایجاد سرویس پردازشگر صف (Consumer)
public class QueuedHostedService : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly ILogger<QueuedHostedService> _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger)
{
_taskQueue = taskQueue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "خطا در اجرای آیتم از صف.");
}
}
}
}
گام ۴: استفاده از صف (Producer)
حالا از هر جای برنامه، مانند یک API Controller، میتوانید کاری را به صف اضافه کنید.
[ApiController]
[Route("[controller]")]
public class EmailController : ControllerBase
{
private readonly IBackgroundTaskQueue _queue;
public EmailController(IBackgroundTaskQueue queue)
{
_queue = queue;
}
[HttpPost]
public IActionResult SendEmail()
{
_queue.QueueBackgroundWorkItemAsync(async token =>
{
// شبیهسازی ارسال ایمیل
await Task.Delay(TimeSpan.FromSeconds(5), token);
Console.WriteLine("ایمیل با موفقیت ارسال شد.");
});
return Ok("درخواست ارسال ایمیل به صف اضافه شد.");
}
}
جمعبندی و بهترین شیوهها
-
انتخاب درست: برای اکثر سناریوها، از کلاس BackgroundService به جای پیادهسازی مستقیم IHostedService استفاده کنید.
-
مدیریت Scope: همیشه برای استفاده از سرویسهای Scoped (مانند DbContext) در سرویسهای پسزمینه، یک Scope جدید با IServiceProvider.CreateScope() ایجاد کنید.
-
خاتمهی صحیح: به CancellationToken احترام بگذارید تا از خاموش شدن آرام و بدون مشکل برنامه اطمینان حاصل کنید.
-
مدیریت خطا: تمام منطق خود را در بلوک try-catch قرار دهید تا از کرش کردن برنامه جلوگیری شود.
-
کتابخانههای جانبی: برای نیازهای پیچیدهتر مانند زمانبندیهای پیشرفته، تلاش مجدد (Retry)، و داشبورد مدیریتی، استفاده از کتابخانههایی مانند Hangfire یا Quartz.NET را مد نظر قرار دهید.
-
الگوی Worker Service: برای برنامههایی که تمرکز اصلی آنها بر اجرای کارهای پسزمینه است و نیازی به میزبانی وب (مانند API) ندارند، از الگوی پروژه Worker Service در ویژوال استودیو استفاده کنید. این الگو یک ساختار بهینه برای این نوع کاربردها فراهم میکند.
سرویسهای پسزمینه ابزاری حیاتی در جعبهابزار هر توسعهدهندهی ASP.NET Core هستند که به ساخت برنامههایی مقیاسپذیرتر، پاسخگوتر و قویتر کمک شایانی میکنند.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.