ASP.NET Core cookbook · Intermediate
Run periodic background work with scoped services
A background job needs the DbContext (scoped) and must stop cleanly on shutdown.
Approach
Implement BackgroundService with a PeriodicTimer, create a DI scope per run to resolve scoped services,
catch and log failures so one bad run doesn't kill the loop, and honour the stopping token.
Solution
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ICartCleaner, CartCleaner>();
builder.Services.AddHostedService<ExpiredCartsJob>();
builder.Services.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromSeconds(20));
var app = builder.Build();
app.Run();
public interface ICartCleaner { Task<int> DeleteExpiredAsync(CancellationToken ct); }
public class CartCleaner : ICartCleaner
{
public Task<int> DeleteExpiredAsync(CancellationToken ct) => Task.FromResult(0); // uses a scoped DbContext
}
public class ExpiredCartsJob(IServiceScopeFactory scopes, ILogger<ExpiredCartsJob> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
do
{
try
{
await using var scope = scopes.CreateAsyncScope();
var cleaner = scope.ServiceProvider.GetRequiredService<ICartCleaner>();
var deleted = await cleaner.DeleteExpiredAsync(stoppingToken);
logger.LogInformation("Deleted {Count} expired carts", deleted);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Expired cart cleanup failed");
}
} while (await timer.WaitForNextTickAsync(stoppingToken));
}
}
Pitfalls
- Injecting scoped services (DbContext) directly into the hosted service constructor.
- Unhandled exceptions in
ExecuteAsync, which stop the service (and by default the host). - Running the same job on every replica; use a distributed lock or a dedicated worker deployment.
Variations
- For durable, retryable jobs use a queue (Azure Service Bus, RabbitMQ) or a library like Hangfire or Quartz.NET.
- Use
Channel<T>with a background consumer for in-process work queues.
Further reading: Hosted services
Reference: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services
More ASP.NET Core recipes
- Accept file uploads with size and type limits
- Add OpenTelemetry traces and metrics
- Apply EF Core migrations safely during deployment
- Bind and validate options at startup
- Cache responses with output caching and evict by tag
- Call external APIs with typed HttpClients and resilience
- Configure CORS for a browser front end
- Expose liveness and readiness health checks