C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
135: Building a Background Worker Service
I've been noticing that our application's database is getting cluttered. We're storing session tokens for user logins, but we never actually clean them up. Right now, I'm just ignoring it, but in a production environment, that table would grow until it killed our query performance. I need something that runs in the background, every few minutes, to scrub those expired tokens without blocking the main API.
The "Wait, why is my app frozen?" phase
My first instinct—the lazy engineer in me—was to just throw a while(true) loop inside the Main method of the program. I figured I'd just await Task.Delay() and call it a day. But as soon as I did that, I realized the rest of my application wouldn't even start. The runtime was stuck in my cleanup loop, and the web server never actually launched.
That's where the BackgroundService class comes in. It's designed specifically for this "fire and forget" logic that needs to live alongside your main app. I'll start by creating a class that inherits from it. The only thing I really need to override is ExecuteAsync.
public class SessionCleanupService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// I'll put my logic here
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine("Cleaning up expired sessions...");
await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
}
}
}
I register this in Program.cs using builder.Services.AddHostedService<SessionCleanupService>(). Now, when I run the app, the API starts up immediately, and every ten minutes, I see that log message. It works, but it's not actually *doing* anything yet because I haven't connected it to the database.
The "Dependency Injection" wall
Here is where I hit a snag. I tried to inject my SessionDbContext directly into the constructor of the SessionCleanupService. I hit "Run" and immediately got a runtime exception: "Cannot consume scoped service from singleton."
I forgot a fundamental rule of .NET dependency injection: BackgroundService is registered as a Singleton. It lives for the entire life of the app. However, our database context is Scoped—it's created and destroyed with every HTTP request. You can't put a short-lived object inside a long-lived one; the scoped object would be disposed of almost immediately, leaving the worker holding a dead reference.
To fix this, I can't inject the context. I have to inject the IServiceScopeFactory. This allows me to manually create a temporary "scope" inside the loop, resolve the database context within that scope, and then let it be disposed of once the cleanup is done.
public class SessionCleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public SessionCleanupService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (IServiceScope scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<SessionDbContext>();
var expiredSessions = dbContext.Sessions
.Where(s => s.ExpiryDate < DateTime.UtcNow);
dbContext.Sessions.RemoveRange(expiredSessions);
await dbContext.SaveChangesAsync(stoppingToken);
}
await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken);
}
}
}
Handling the graceful exit
One last thing I noticed: when I stopped the application during debugging, it sometimes felt like it took a few seconds to actually shut down, or it just killed the process abruptly. I realized I wasn't passing the stoppingToken into SaveChangesAsync.
The CancellationToken is vital. If the OS tells the app to shut down, the host signals that token. If my service is in the middle of a massive database delete, passing that token ensures the database operation is canceled gracefully rather than leaving the transaction in a weird state or hanging the process until the OS forces it closed.
Now the service is robust. It doesn't block the API, it handles its own database scopes correctly, and it shuts down cleanly when the app stops. It's a simple pattern, but it's the backbone of almost every "maintenance" task in a professional C# backend.
📋 Practical Task
Build an Automated Temporary File Purger
Your task is to create a background worker service that manages a local "temp" directory. This mimics a real-world scenario where an app generates PDF exports or image thumbnails that shouldn't stay on the disk forever.
- The Setup: Create a
FilePurgeServiceinheriting fromBackgroundService. - The Logic: Every 30 seconds, the service should scan a specific folder on your hard drive (create a folder named
C:\TempAppFilesor similar for this test). - The Rule: Any file older than 60 seconds should be deleted.
- The Constraint: Use a
try-catchblock inside the loop. This is crucial because files might be locked by another process (like an open text editor), and you don't want a single locked file to crash your entire background service. - The Shutdown: Ensure you are passing the
stoppingTokento any async methods used within the loop.
Test it by manually adding a few files to the folder and observing the console logs as the service wakes up and deletes them once they expire.
There are no comments for now.