IHostedService and BackgroundService
IHostedService is the core abstraction ASP.NET Core's generic host uses to run background work alongside the web server: it defines StartAsync(CancellationToken) and StopAsync(CancellationToken), which the host invokes when the application starts up and shuts down respectively. Rather than implementing IHostedService directly, most real-world background workers derive from the abstract BackgroundService class, which implements IHostedService for you and exposes a single ExecuteAsync(CancellationToken stoppingToken) method containing your actual work loop — typically a while (!stoppingToken.IsCancellationRequested) loop with an await Task.Delay between iterations.
Cricket analogy: IHostedService is like the ground staff crew who prepare the pitch before play starts and cover it after stumps — StartAsync is rolling the pitch before the first ball, StopAsync is covering it once the day's play ends, running independently of the actual match on the field.
Background services are registered with builder.Services.AddHostedService<T>() where T derives from BackgroundService, and they participate in the same DI container as everything else, but because a BackgroundService itself typically has an implicit Singleton-like lifetime bound to the host, it cannot directly inject Scoped services such as a repository or DbContext through its constructor — doing so causes the captive dependency problem described in the service-lifetimes topic. The standard fix is injecting IServiceScopeFactory and creating a new scope inside each loop iteration, ensuring every unit of background work gets a fresh, properly-scoped DbContext just like a real HTTP request would.
Cricket analogy: This mirrors a groundskeeper (background service) needing a fresh set of pitch-preparation tools (Scoped dependency) checked out for every single match day rather than keeping one permanently borrowed set that goes stale — IServiceScopeFactory is the equipment room issuing a fresh checkout each time.
Scheduling and Graceful Shutdown
A common pattern is a periodic worker that runs a task every N minutes using PeriodicTimer (preferred over Task.Delay in a loop since .NET 6, because it avoids drift and integrates cleanly with cancellation), while more complex scheduling needs — like cron-style expressions — are typically handled by a library such as Quartz.NET or Hangfire rather than hand-rolled timer logic. Graceful shutdown matters because the host gives running IHostedService instances a limited shutdown timeout (configurable via HostOptions.ShutdownTimeout, five seconds by default) to finish StopAsync before the process is forcibly terminated, so long-running loops must check stoppingToken.IsCancellationRequested frequently and avoid work that can't be interrupted within that window.
Cricket analogy: A PeriodicTimer worker is like a scheduled drinks break called at fixed intervals during a match rather than an umpire eyeballing the clock loosely — it's a disciplined, drift-free schedule; graceful shutdown is like the umpire giving fielders a firm but bounded window to leave the field once stumps are called.
public class EnrollmentDigestWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<EnrollmentDigestWorker> _logger;
public EnrollmentDigestWorker(IServiceScopeFactory scopeFactory, ILogger<EnrollmentDigestWorker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(15));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using var scope = _scopeFactory.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<ICourseRepository>();
var mailer = scope.ServiceProvider.GetRequiredService<IEmailSender>();
var newEnrollments = await repo.GetRecentEnrollmentsAsync(stoppingToken);
if (newEnrollments.Count > 0)
await mailer.SendDigestAsync(newEnrollments, stoppingToken);
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(ex, "Enrollment digest run failed");
}
}
}
}
// Program.cs
builder.Services.AddHostedService<EnrollmentDigestWorker>();
A BackgroundService whose ExecuteAsync throws an unhandled exception stops permanently and, depending on HostOptions.BackgroundServiceExceptionBehavior, can even crash the entire host process. Always wrap the loop body in a try/catch that logs and continues, and reserve unhandled exceptions only for truly fatal, unrecoverable startup failures.
- IHostedService defines StartAsync/StopAsync; BackgroundService is the common base class exposing ExecuteAsync for the work loop.
- Register background workers with AddHostedService<T>(), which wires them into the generic host's start/stop lifecycle.
- A BackgroundService cannot safely inject Scoped services directly; use IServiceScopeFactory.CreateScope() per work unit instead.
- PeriodicTimer is preferred over a raw Task.Delay loop since .NET 6 because it avoids timing drift and integrates with cancellation.
- Complex cron-style scheduling is usually delegated to a library like Quartz.NET or Hangfire rather than hand-rolled.
- HostOptions.ShutdownTimeout bounds how long StopAsync has to finish before the process is forcibly terminated (5 seconds by default).
- Catch and log exceptions inside the work loop so one failed iteration doesn't silently stop the worker or crash the host.
Practice what you learned
1. What method does BackgroundService expose for you to implement your actual background work?
2. Why can't a BackgroundService safely inject a Scoped service like a repository directly through its constructor?
3. What is the benefit of PeriodicTimer over a loop using Task.Delay?
4. What controls how long a hosted service's StopAsync has to complete before the process is forcibly terminated?
5. Why should exceptions inside a BackgroundService's work loop typically be caught and logged rather than left unhandled?
Was this page helpful?
You May Also Like
Service Lifetimes: Singleton, Scoped, Transient
How ASP.NET Core's built-in dependency injection container manages object lifetime through Singleton, Scoped, and Transient registrations.
Entity Framework Core Integration
How to wire Entity Framework Core into an ASP.NET Core application, from DbContext registration to migrations and query patterns.
Caching Strategies: In-Memory and Distributed
How to use IMemoryCache and IDistributedCache in ASP.NET Core to reduce database load, and how to choose between them.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics