🏭 Background Services in ASP.NET
Your App’s Invisible Helpers That Never Sleep
The Factory Analogy 🏢
Imagine your ASP.NET app is a busy factory. When customers visit (HTTP requests), workers help them right away. But some jobs need to happen behind the scenes—cleaning floors at night, checking inventory, sending reminder emails.
These behind-the-scenes workers are Background Services. They work quietly, never facing customers, but keeping everything running smoothly.
🎯 What You’ll Learn
graph LR A["Background Services"] --> B["Hosted Services"] A --> C["IHostedService"] A --> D["BackgroundService Class"] A --> E["Service Lifecycle"] A --> F["Worker Services"] A --> G["Timed Tasks"] A --> H["Scoped Services"]
1️⃣ Hosted Services: The Night Shift Workers
What Are They?
A Hosted Service is any background worker that runs alongside your main app. Think of it like a janitor who starts work when the factory opens and leaves when it closes.
Simple Example
public class CleanupService
: IHostedService
{
public Task StartAsync(
CancellationToken ct)
{
Console.WriteLine(
"Cleanup started!");
return Task.CompletedTask;
}
public Task StopAsync(
CancellationToken ct)
{
Console.WriteLine(
"Cleanup stopped!");
return Task.CompletedTask;
}
}
Register It
// In Program.cs
builder.Services
.AddHostedService<CleanupService>();
Real Life: Email sender, cache warmer, health checker—all hosted services!
2️⃣ IHostedService: The Contract
The Promise Every Background Worker Makes
IHostedService is like a job description. Every background worker must:
- Start when the app starts (
StartAsync) - Stop gracefully when the app shuts down (
StopAsync)
graph TD A["App Starts"] --> B["StartAsync Called"] B --> C["Worker Runs..."] C --> D["App Shutting Down"] D --> E["StopAsync Called"] E --> F["Clean Exit"]
The Interface
public interface IHostedService
{
Task StartAsync(
CancellationToken ct);
Task StopAsync(
CancellationToken ct);
}
Think of it like: A promise to show up on time and leave properly—no ghosting!
3️⃣ BackgroundService Class: The Easy Button
Why Reinvent the Wheel?
Writing IHostedService from scratch gets repetitive. The BackgroundService class gives you a shortcut!
Instead of implementing both methods, just override one: ExecuteAsync.
Before (Manual Way)
public class MyWorker : IHostedService
{
private Task? _task;
public Task StartAsync(
CancellationToken ct)
{
_task = DoWorkAsync(ct);
return Task.CompletedTask;
}
public async Task StopAsync(
CancellationToken ct)
{
if (_task != null)
await _task;
}
private async Task DoWorkAsync(
CancellationToken ct) { }
}
After (Easy Way) ✨
public class MyWorker
: BackgroundService
{
protected override async Task
ExecuteAsync(
CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
// Do your work here!
await Task.Delay(1000, ct);
}
}
}
Magic! Less code, same power. The BackgroundService handles start/stop for you.
4️⃣ Hosted Service Lifecycle: Birth to Goodbye
The Journey of a Background Worker
graph TD A["🌱 Created"] --> B["📦 Registered"] B --> C["🚀 StartAsync"] C --> D["⚡ Running"] D --> E["🛑 StopAsync"] E --> F["🪦 Disposed"]
Key Moments
| Phase | What Happens |
|---|---|
| Created | Object is born |
| StartAsync | Worker begins |
| Running | Doing its job |
| StopAsync | Graceful exit |
| Disposed | Memory freed |
Important Rule
⚠️ StartAsync must return quickly! Don’t block—start your long-running work and return.
// ✅ Good: Return immediately
public Task StartAsync(
CancellationToken ct)
{
_task = RunAsync(ct);
return Task.CompletedTask;
}
// ❌ Bad: Blocking forever
public Task StartAsync(
CancellationToken ct)
{
while (true) { } // NEVER DO THIS!
}
5️⃣ Worker Services: Standalone Background Apps
What If Background Work Is ALL You Need?
Sometimes you don’t need a web server—just a program that runs tasks forever. That’s a Worker Service!
Create One
dotnet new worker -n MyWorker
What You Get
public class Worker : BackgroundService
{
private readonly ILogger<Worker>
_logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task
ExecuteAsync(
CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
_logger.LogInformation(
"Working at: {time}",
DateTimeOffset.Now);
await Task.Delay(1000, ct);
}
}
}
Real Uses
- 📧 Process email queues
- 📊 Sync data between systems
- 🔍 Monitor files for changes
- 📨 Handle message queues
6️⃣ Timed Background Tasks: The Alarm Clock
Running Tasks on a Schedule
Want something to run every 5 minutes? Every hour? Use a timer!
public class TimedService
: BackgroundService
{
private readonly TimeSpan _period =
TimeSpan.FromMinutes(5);
protected override async Task
ExecuteAsync(
CancellationToken ct)
{
using var timer =
new PeriodicTimer(_period);
while (!ct.IsCancellationRequested &&
await timer
.WaitForNextTickAsync(ct))
{
await DoScheduledWork();
}
}
private Task DoScheduledWork()
{
Console.WriteLine(
quot;Task ran at {DateTime.Now}");
return Task.CompletedTask;
}
}
Timer Options
| Method | Best For |
|---|---|
PeriodicTimer |
.NET 6+, clean async |
Timer |
Older .NET, callbacks |
Task.Delay |
Simple delays |
7️⃣ Scoped Services in Background: The Tricky Part
The Problem
Background services are singletons—they live forever. But some services (like database contexts) are scoped—they live per-request.
You can’t inject scoped services directly into singletons!
// ❌ This will CRASH!
public class BadWorker : BackgroundService
{
private readonly MyDbContext _db;
public BadWorker(MyDbContext db)
{
_db = db; // BOOM! Scope error!
}
}
The Solution: Create a Scope
Inject IServiceScopeFactory and create scopes manually:
public class GoodWorker
: BackgroundService
{
private readonly
IServiceScopeFactory _scopeFactory;
public GoodWorker(
IServiceScopeFactory factory)
{
_scopeFactory = factory;
}
protected override async Task
ExecuteAsync(
CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using (var scope =
_scopeFactory.CreateScope())
{
var db = scope
.ServiceProvider
.GetRequiredService
<MyDbContext>();
// Now use db safely!
await db.SaveChangesAsync();
}
await Task.Delay(5000, ct);
}
}
}
graph TD A["Background Service"] --> B["Create Scope"] B --> C["Get Scoped Service"] C --> D["Do Work"] D --> E["Dispose Scope"] E --> F["Wait"] F --> B
🎁 Quick Reference
| Concept | One-Liner |
|---|---|
| Hosted Service | Background worker tied to app lifetime |
| IHostedService | Interface with StartAsync + StopAsync |
| BackgroundService | Base class with just ExecuteAsync |
| Lifecycle | Create → Start → Run → Stop → Dispose |
| Worker Service | Standalone app for background work |
| Timed Tasks | Use PeriodicTimer for schedules |
| Scoped Services | Use IServiceScopeFactory to create scopes |
🚀 You Did It!
You now understand how ASP.NET apps run tasks in the background! Like invisible factory workers, these services keep your app healthy, send emails, process queues, and much more—all without users ever seeing them.
Remember: Background services are your app’s silent heroes. Treat them well! 🦸♀️
