Core API Background Services
Some tasks in the BookStore API do not belong in an HTTP request-response cycle. Sending daily sales reports, clearing expired cache entries, checking for books with low stock, or generating daily summaries are tasks that should run on a schedule in the background — not triggered by a client request. ASP.NET Core provides Background Services (also called Hosted Services) for exactly this purpose.
What Is a Background Service?
A background service is a long-running process that starts when the application starts and stops when the application stops. It runs independently of HTTP requests — even if no client sends any request, the background service keeps running.
Application starts
|
├── HTTP Pipeline → handles client requests
│
└── Background Services (run in parallel)
├── DailyReportService → sends email every midnight
└── CacheCleanupService → removes stale cache every hour
IHostedService vs BackgroundService
| Interface | What It Provides | Use When |
|---|---|---|
IHostedService | Raw StartAsync and StopAsync methods | Full control needed |
BackgroundService | Abstract class with ExecuteAsync to implement | Most common — simpler to implement |
BackgroundService is the easier option — implement just one method: ExecuteAsync.
Example 1 – Book Availability Check Service
This background service checks every hour for books that have been marked unavailable and logs a report.
// Services/BookAvailabilityCheckerService.cs
using BookStoreAPI.Data;
using Microsoft.EntityFrameworkCore;
namespace BookStoreAPI.Services
{
public class BookAvailabilityCheckerService : BackgroundService
{
private readonly ILogger<BookAvailabilityCheckerService> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly TimeSpan _interval = TimeSpan.FromHours(1);
public BookAvailabilityCheckerService(
ILogger<BookAvailabilityCheckerService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Book Availability Checker started.");
while (!stoppingToken.IsCancellationRequested)
{
await CheckUnavailableBooksAsync();
// Wait 1 hour before running again
await Task.Delay(_interval, stoppingToken);
}
_logger.LogInformation("Book Availability Checker stopped.");
}
private async Task CheckUnavailableBooksAsync()
{
_logger.LogInformation("[{Time}] Checking for unavailable books...", DateTime.UtcNow);
// Background services must create their own scope to use Scoped services
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BookStoreDbContext>();
var unavailableBooks = await dbContext.Books
.Where(b => !b.IsAvailable)
.ToListAsync();
if (unavailableBooks.Count == 0)
{
_logger.LogInformation("All books are currently available.");
return;
}
_logger.LogWarning("{Count} book(s) are currently unavailable:", unavailableBooks.Count);
foreach (var book in unavailableBooks)
{
_logger.LogWarning(" - [{Id}] {Title} by {Author}", book.Id, book.Title, book.Author);
}
}
}
}
Example 2 – Daily Summary Service
This service generates a daily count of books in the database at midnight every day.
// Services/DailySummaryService.cs
namespace BookStoreAPI.Services
{
public class DailySummaryService : BackgroundService
{
private readonly ILogger<DailySummaryService> _logger;
private readonly IServiceProvider _serviceProvider;
public DailySummaryService(
ILogger<DailySummaryService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Daily Summary Service started.");
while (!stoppingToken.IsCancellationRequested)
{
// Calculate time until next midnight
var now = DateTime.UtcNow;
var nextMidnight = now.Date.AddDays(1);
var timeUntilMidnight = nextMidnight - now;
_logger.LogInformation("Daily summary will run in {Hours}h {Minutes}m.",
(int)timeUntilMidnight.TotalHours, timeUntilMidnight.Minutes);
// Wait until midnight
await Task.Delay(timeUntilMidnight, stoppingToken);
if (!stoppingToken.IsCancellationRequested)
{
await GenerateDailySummaryAsync();
}
}
}
private async Task GenerateDailySummaryAsync()
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BookStoreDbContext>();
var totalBooks = await dbContext.Books.CountAsync();
var availableBooks = await dbContext.Books.CountAsync(b => b.IsAvailable);
var categories = await dbContext.Books
.GroupBy(b => b.Category)
.Select(g => new { Category = g.Key, Count = g.Count() })
.ToListAsync();
_logger.LogInformation("=== BookStore Daily Summary ({Date}) ===", DateTime.UtcNow.Date);
_logger.LogInformation("Total books: {Total}", totalBooks);
_logger.LogInformation("Available books: {Available}", availableBooks);
_logger.LogInformation("Unavailable books: {Unavailable}", totalBooks - availableBooks);
foreach (var cat in categories)
{
_logger.LogInformation(" Category '{Category}': {Count} books", cat.Category, cat.Count);
}
}
}
}
Registering Background Services in Program.cs
// Program.cs
builder.Services.AddHostedService<BookAvailabilityCheckerService>();
builder.Services.AddHostedService<DailySummaryService>();
Why IServiceProvider Instead of Direct Injection?
Background services are registered as Singleton — they live for the entire application lifetime. However, BookStoreDbContext is registered as Scoped — it lives only for the duration of one HTTP request.
A Singleton cannot directly depend on a Scoped service — this causes a runtime error. The solution is to inject IServiceProvider and create a scope manually each time the background service needs to access the database.
Singleton (Background Service)
|
| ← Cannot inject Scoped services directly!
|
IServiceProvider → CreateScope() → new scope → resolve Scoped DbContext
// Pattern: always use CreateScope() to access Scoped services in background services
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<BookStoreDbContext>();
// use dbContext...
// scope disposes automatically at the end of the using block
Handling Cancellation Gracefully
The stoppingToken is signaled when the application is shutting down. Checking it prevents the service from starting a new task after shutdown begins:
while (!stoppingToken.IsCancellationRequested)
{
// Do work...
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
Passing stoppingToken to Task.Delay() means the delay is cancelled immediately when the app stops — no waiting for the full hour.
Background Service Lifecycle
App starts
→ BackgroundService.StartAsync() is called automatically
→ ExecuteAsync() begins running
App receives requests normally
→ Background service runs independently (on its own thread)
App stops (Ctrl+C or deployment restart)
→ CancellationToken is signaled
→ ExecuteAsync() exits the while loop
→ BackgroundService.StopAsync() cleans up
Key Points
- Background services run independently of HTTP requests — they start with the app and stop when the app stops.
- Inherit from
BackgroundServiceand implementExecuteAsync— this is the recommended approach. - Register background services with
AddHostedService<T>()inProgram.cs. - Background services are Singletons — use
IServiceProvider.CreateScope()to access Scoped services likeDbContext. - Always check
stoppingToken.IsCancellationRequestedin the loop and pass the token toTask.Delay()for graceful shutdown.
