⏰ Long-Running Tasks in ASP.NET Core
Need scheduled jobs? Background processing? Email queue? IHostedService runs tasks in background without blocking requests.
Basic Background Service
public class EmailQueueService : BackgroundService
{
private readonly ILogger _logger;
private readonly IServiceProvider _services;
public EmailQueueService(
ILogger logger,
IServiceProvider services)
{
_logger = logger;
_services = services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Email Queue Service started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
// Create scope for scoped services
using var scope = _services.CreateScope();
var emailService = scope.ServiceProvider
.GetRequiredService();
// Process emails
await emailService.ProcessQueueAsync();
// Wait 30 seconds before next run
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing email queue");
}
}
_logger.LogInformation("Email Queue Service stopped");
}
}
// Program.cs - Register service
builder.Services.AddHostedService();
🎯 Scheduled Task Example
public class DailyReportService : BackgroundService
{
private readonly ILogger _logger;
private readonly IServiceProvider _services;
public DailyReportService(
ILogger logger,
IServiceProvider services)
{
_logger = logger;
_services = services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Calculate time until next 9 AM
var now = DateTime.Now;
var nextRun = DateTime.Today.AddHours(9);
if (now > nextRun)
{
nextRun = nextRun.AddDays(1);
}
var delay = nextRun - now;
_logger.LogInformation($"Next report at {nextRun}");
// Wait until 9 AM
await Task.Delay(delay, stoppingToken);
// Generate report
using var scope = _services.CreateScope();
var reportService = scope.ServiceProvider
.GetRequiredService();
await reportService.GenerateDailyReportAsync();
}
}
}
IHostedService vs BackgroundService
IHostedService
// Manual implementation
public class MyService : IHostedService
{
public Task StartAsync(
CancellationToken ct)
{
// Start logic
return Task.CompletedTask;
}
public Task StopAsync(
CancellationToken ct)
{
// Cleanup
return Task.CompletedTask;
}
}
Use when: Need full control
BackgroundService
// Easier implementation
public class MyService
: BackgroundService
{
protected override async Task
ExecuteAsync(
CancellationToken ct)
{
// Long-running work
while (!ct.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(1000, ct);
}
}
}
Use when: Long-running loops
Real-World Use Cases
📧 Email Queue Processor
- Checks database for pending emails every 30 seconds
- Sends emails in background
- Marks as sent/failed in database
- Doesn’t block web requests
🗑️ Cleanup Service
- Runs every night at 2 AM
- Deletes old temp files
- Archives old database records
- Clears expired cache entries
📊 Data Sync Service
- Polls external API every 5 minutes
- Syncs data to local database
- Updates cache
- Notifies connected clients via SignalR
💡 Best Practices
- Use scopes for DI: Hosted services are singletons, create scopes for scoped services
- Handle exceptions: Wrap work in try-catch, log errors
- Respect cancellation: Check
stoppingToken.IsCancellationRequested - Graceful shutdown: Clean up resources in StopAsync
- Don’t block startup: Long initialization? Do it async in ExecuteAsync
⚠️ Important Notes
- Not for heavy work: For CPU-intensive tasks, use separate worker service
- No guaranteed execution: App restart = service stops
- Consider Hangfire/Quartz: For complex scheduling, use dedicated libraries
- Health checks: Add health check endpoint to monitor service status
“Moved email sending to background service. API responses instant now. Emails send reliably in background. Added retry logic. Zero impact on user experience. Should’ve done this years ago.”
