1. What is a Worker Service?
A Worker Service is a .NET application designed to run continuously in the background.
Unlike an ASP.NET Core Web API:
Web API
↓
HTTP Request
↓
Controller
↓
Business Logic
↓
HTTP ResponseA Worker Service generally works like:
Worker Service
↓
Background Process
↓
Read Message / Timer / Event
↓
Business Logic
↓
Database / External API
↓
Continue ProcessingA Worker Service can run as:
Windows Service
Linux systemd service
Docker container
Kubernetes Pod
Azure Container Apps
Azure App Service background process in appropriate hosting models
VM-hosted process
Kubernetes CronJob for scheduled/batch work
2. Why do we need Worker Services?
Consider an e-commerce application.
A customer places an order:
Customer
↓
Order API
↓
Create Order
↓
Return responseBut after creating the order, many things may need to happen:
Order Created
│
├── Send Email
├── Generate Invoice
├── Update Inventory
├── Send Notification
├── Create Shipment
└── Update AnalyticsDoing all of these inside the API request can make the API slow.
Instead:
┌───────────────┐
Customer ───────►│ Order API │
└───────┬───────┘
│
│ OrderCreated
▼
┌───────────────┐
│ Message Queue │
└───────┬───────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Email Worker Inventory Worker Invoice WorkerThis is where Worker Services become extremely useful.
3. Worker Service vs BackgroundService
These two terms are related but not exactly the same.
Worker Service
A Worker Service is a .NET project/application template designed for long-running background workloads.
You can create one using:
dotnet new worker -n OrderProcessing.WorkerIt normally contains:
OrderProcessing.Worker
│
├── Program.cs
├── Worker.cs
└── appsettings.jsonBackgroundService
BackgroundService is a base class provided by .NET for implementing long-running background work.
For example:
public class Worker : BackgroundService
{
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Console.WriteLine("Worker is running...");
await Task.Delay(
TimeSpan.FromSeconds(5),
stoppingToken);
}
}
}So:
Worker Service
│
└── Hosting Model
│
└── BackgroundService
│
└── ExecuteAsync()4. Basic Worker Service Architecture
A typical Worker Service looks like this:
┌─────────────────────────────────────┐
│ .NET Worker Service │
│ │
│ Generic Host │
│ │ │
│ ├── Dependency Injection │
│ ├── Configuration │
│ ├── Logging │
│ ├── Configuration │
│ └── Hosted Services │
│ │ │
│ ▼ │
│ BackgroundService │
│ │ │
│ ▼ │
│ ExecuteAsync() │
│ │ │
│ ▼ │
│ Business Processing │
└─────────────────────────────────────┘5. Creating a Worker Service
Create the project:
dotnet new worker -n OrderProcessing.WorkerMove into the project:
cd OrderProcessing.WorkerRun it:
dotnet runThe default template will create something similar to:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation(
"Worker running at: {time}",
DateTimeOffset.Now);
await Task.Delay(
1000,
stoppingToken);
}
}
}6. Understanding ExecuteAsync()
This is the most important method.
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)The Worker Service calls this method when the application starts.
For example:
Application Starts
↓
Host Starts
↓
Worker Starts
↓
ExecuteAsync()
↓
while loop
↓
Background processing7. Why CancellationToken is important
Suppose your worker is running:
Worker
↓
Processing
↓
Processing
↓
ProcessingNow Kubernetes sends a termination signal.
The worker should stop gracefully.
That's why we use:
CancellationToken stoppingTokenExample:
while (!stoppingToken.IsCancellationRequested)
{
await ProcessOrderAsync(stoppingToken);
}When cancellation occurs:
Kubernetes
↓
SIGTERM
↓
CancellationToken
↓
Worker stopsThis is especially important in production microservices.
8. Real-Time Example — Order Processing Microservice
Let's build a realistic architecture.
Imagine an e-commerce system:
Customer
↓
Order API
↓
Azure Service Bus
↓
Order Processing Worker
↓
Order DatabaseThe API doesn't need to process everything synchronously.
9. Step 1 — Order API
The API receives:
POST /api/ordersRequest:
{
"customerId": 1001,
"productId": 5001,
"quantity": 2
}The API creates the order.
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder(
CreateOrderRequest request)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = request.CustomerId,
ProductId = request.ProductId,
Quantity = request.Quantity,
Status = "Pending"
};
// Save order
// Publish OrderCreated event
return Accepted(order);
}
}Notice:
return Accepted(order);The API doesn't need to wait for the complete background processing.
10. Step 2 — Create an Event
Create:
public class OrderCreatedEvent
{
public Guid OrderId { get; set; }
public int CustomerId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
}The API publishes:
OrderCreatedEventto a message broker.
For example:
Azure Service BusArchitecture:
Order API
│
│ OrderCreatedEvent
▼
Azure Service Bus
│
▼
Order Processing Worker11. Step 3 — Create Worker Service
Create:
dotnet new worker -n OrderProcessing.WorkerInstall the Azure Service Bus package:
dotnet add package Azure.Messaging.ServiceBus12. Configure Service Bus
appsettings.json:
{
"ServiceBus": {
"ConnectionString": "YOUR_CONNECTION_STRING",
"QueueName": "orders"
}
}In production, don't put secrets directly into appsettings.json.
Use:
Azure Key Vault
Managed Identity
Environment variables
Kubernetes Secrets
13. Create Worker
public class OrderWorker : BackgroundService
{
private readonly ILogger<OrderWorker> _logger;
private readonly ServiceBusProcessor _processor;
public OrderWorker(
IConfiguration configuration,
ILogger<OrderWorker> logger)
{
_logger = logger;
var connectionString =
configuration["ServiceBus:ConnectionString"];
var queueName =
configuration["ServiceBus:QueueName"];
var client = new ServiceBusClient(connectionString);
_processor = client.CreateProcessor(queueName);
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
_processor.ProcessMessageAsync += ProcessMessage;
_processor.ProcessErrorAsync += ProcessError;
await _processor.StartProcessingAsync(
stoppingToken);
try
{
await Task.Delay(
Timeout.Infinite,
stoppingToken);
}
catch (OperationCanceledException)
{
// Application shutting down
}
await _processor.StopProcessingAsync();
}
private async Task ProcessMessage(
ProcessMessageEventArgs args)
{
var messageBody =
args.Message.Body.ToString();
_logger.LogInformation(
"Received Order: {Message}",
messageBody);
// Process order
await args.CompleteMessageAsync(args.Message);
}
private Task ProcessError(
ProcessErrorEventArgs args)
{
_logger.LogError(
args.Exception,
"Error processing message");
return Task.CompletedTask;
}
}14. Register Worker in Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<OrderWorker>();
var host = builder.Build();
host.Run();This is the key line:
builder.Services.AddHostedService<OrderWorker>();It tells .NET:
Start this Worker when the application starts.
15. Complete Processing Flow
Now the entire flow becomes:
CUSTOMER
│
▼
┌──────────────┐
│ Order API │
└──────┬───────┘
│
│ Save Order
▼
┌──────────────┐
│ Azure SQL DB │
└──────────────┘
│
│ Publish Event
▼
┌───────────────────┐
│ Azure Service Bus│
│ orders │
└─────────┬─────────┘
│
▼
┌─────────────────────┐
│ Order Worker Service│
└──────────┬──────────┘
│
┌────────┼─────────┐
▼ ▼ ▼
Inventory Payment NotificationThis is a very common microservice architecture.
16. Worker Service with Dependency Injection
Don't put all business logic inside:
Worker.csInstead:
Worker
│
▼
OrderProcessor
│
├── OrderRepository
├── InventoryService
├── PaymentService
└── NotificationServiceExample:
public interface IOrderProcessor
{
Task ProcessAsync(
OrderCreatedEvent order,
CancellationToken cancellationToken);
}Implementation:
public class OrderProcessor : IOrderProcessor
{
private readonly ILogger<OrderProcessor> _logger;
public OrderProcessor(
ILogger<OrderProcessor> logger)
{
_logger = logger;
}
public async Task ProcessAsync(
OrderCreatedEvent order,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Processing Order {OrderId}",
order.OrderId);
// Business logic
await Task.CompletedTask;
}
}Register:
builder.Services.AddScoped<IOrderProcessor, OrderProcessor>();
builder.Services.AddHostedService<OrderWorker>();17. Important Issue — Scoped Services
This is a very important interview question.
A Worker Service itself is generally effectively singleton-like because it lives for the lifetime of the host.
But services such as:
DbContext
Repository
UnitOfWorkare usually:
ScopedYou shouldn't inject a scoped DbContext directly into a long-lived singleton worker.
Instead use:
IServiceScopeFactoryExample:
public class OrderWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public OrderWorker(
IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope =
_scopeFactory.CreateScope();
var processor =
scope.ServiceProvider
.GetRequiredService<IOrderProcessor>();
await processor.ProcessAsync(
stoppingToken);
await Task.Delay(
TimeSpan.FromSeconds(10),
stoppingToken);
}
}
}This creates a new DI scope for each processing cycle.
18. Worker + Entity Framework Core
For example:
public class OrderProcessor : IOrderProcessor
{
private readonly ApplicationDbContext _db;
public OrderProcessor(ApplicationDbContext db)
{
_db = db;
}
public async Task ProcessAsync(
CancellationToken cancellationToken)
{
var orders = await _db.Orders
.Where(x => x.Status == "Pending")
.ToListAsync(cancellationToken);
foreach (var order in orders)
{
order.Status = "Processed";
}
await _db.SaveChangesAsync(
cancellationToken);
}
}Register:
builder.Services.AddDbContext<ApplicationDbContext>(
options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString(
"DefaultConnection")));19. Worker Service for Scheduled Processing
Workers aren't limited to queues.
You can also execute tasks periodically.
Example:
public class ReportWorker : BackgroundService
{
private readonly ILogger<ReportWorker> _logger;
public ReportWorker(
ILogger<ReportWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(
TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(
stoppingToken))
{
await GenerateReportAsync(
stoppingToken);
}
}
private async Task GenerateReportAsync(
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Generating report at {Time}",
DateTimeOffset.Now);
await Task.CompletedTask;
}
}20. Worker Service for File Processing
Another real-world example:
Customer uploads CSV
↓
Blob Storage
↓
Queue
↓
Worker
↓
Read CSV
↓
Validate records
↓
Insert database
↓
Move file to ProcessedThis is excellent for Worker Services because processing can take several minutes.
21. Worker Service for Email Processing
Architecture:
Application
│
▼
Email Queue
│
▼
Email Worker
│
├── Read message
├── Validate
├── Send email
└── Complete messageThis prevents email processing from slowing down the main API.
22. Worker Service in Microservices
Suppose your system contains:
Order Service
Payment Service
Inventory Service
Notification Service
Shipping ServiceYou can have:
Order API
│
▼
Order Worker
Payment Worker
Inventory Worker
Notification Worker
Shipping WorkerEach worker can be independently deployed and scaled.
This follows an important microservice principle:
A microservice should own a specific business capability.
23. Scaling Workers
Suppose there are:
100 messages/hourOne worker might be enough.
But suppose:
100,000 messages/hourYou can run:
Queue
│
┌───────┼───────┐
▼ ▼ ▼
Worker 1 Worker 2 Worker 3In Kubernetes:
Deployment
│
├── Pod 1
├── Pod 2
├── Pod 3
└── Pod 4Multiple workers consume messages from the same queue.
The broker distributes messages between consumers.
24. Worker Service + Kubernetes
A Worker can run as a container.
Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish \
-c Release \
-o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "OrderProcessing.Worker.dll"]Then:
Docker Image
↓
Azure Container Registry
↓
AKS
↓
Worker Pod25. Worker Deployment in Kubernetes
Example:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-worker
spec:
replicas: 3
selector:
matchLabels:
app: order-worker
template:
metadata:
labels:
app: order-worker
spec:
containers:
- name: order-worker
image: myregistry/order-worker:1.0
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"Now Kubernetes runs:
Order Worker
│
├── Pod 1
├── Pod 2
└── Pod 326. Worker Service Failure Handling
Production workers must handle failures.
For example:
Worker
↓
Process Order
↓
Payment API
↓
FAILEDDon't simply crash the entire application.
Use:
Retry
↓
Retry
↓
Retry
↓
Still Failed
↓
Dead Letter QueueFor example:
Orders Queue
│
▼
Worker
│
├── Success → Complete
│
└── Failure
│
▼
Retry
│
▼
Max Retries
│
▼
Dead Letter Queue27. Idempotency — Extremely Important
Imagine the worker receives:
OrderId = 1001It processes it.
But before acknowledgement:
Worker crashesThe message may be delivered again.
Now:
Order 1001
↓
Processed
↓
Worker crashes
↓
Message delivered again
↓
Order 1001 processed againTherefore your worker should be idempotent.
For example:
var alreadyProcessed =
await _db.ProcessedMessages
.AnyAsync(
x => x.MessageId == messageId,
cancellationToken);
if (alreadyProcessed)
{
return;
}Then:
Message
↓
Check MessageId
↓
Already processed?
│
├── Yes → Skip
│
└── No → ProcessThis is extremely important in distributed systems.
28. Worker + Saga Pattern
Workers are also commonly used with Saga-based microservices.
Example:
Order Created
↓
Order Worker
↓
Payment
↓
Inventory
↓
ShippingIf inventory fails:
Order
↓
Payment SUCCESS
↓
Inventory FAILED
↓
Compensation
↓
Refund Payment
↓
Cancel OrderWorkers can process these asynchronous commands/events.
29. Worker Service vs API
| Feature | Web API | Worker Service |
|---|---|---|
| HTTP endpoint | Yes | No |
| Long-running process | Not ideal | Excellent |
| Queue consumer | Possible | Excellent |
| Scheduled jobs | Possible | Excellent |
| Background processing | Limited | Excellent |
| Request/response | Yes | No |
| Kubernetes | Yes | Yes |
| Docker | Yes | Yes |
| Microservices | Yes | Yes |
30. Alternatives to Worker Services
Worker Services are not the only solution.
Depending on your requirement, you can use several alternatives.
1. Hangfire
Excellent for:
Background Jobs
Scheduled Jobs
Retries
Recurring Jobs
DashboardArchitecture:
API
↓
Hangfire
↓
Background Job
↓
DatabaseGood when you need:
Run every day at 2 AMor:
Run this job in background31. Quartz.NET
Quartz.NET is useful for sophisticated scheduling.
Example:
Daily
Hourly
Weekly
Cron
Complex schedulesArchitecture:
Application
↓
Quartz Scheduler
↓
Job
↓
Business LogicGood for complex scheduling requirements.
32. Azure Functions
If you're already using Azure, Azure Functions can be an excellent alternative.
For example:
Service Bus
↓
Azure Function
↓
Process OrderOr:
Timer
↓
Azure Function
↓
Generate ReportAdvantages:
Serverless
Automatic scaling
Event-driven
Less infrastructure management
33. Azure Service Bus Trigger
A common architecture:
Order API
↓
Azure Service Bus
↓
Azure Function
↓
Process OrderInstead of maintaining a continuously running Worker Service, Azure manages the execution environment.
34. Kubernetes CronJob
For batch jobs that run at a specific time:
Every night 2 AM
↓
Kubernetes CronJob
↓
Create Pod
↓
Execute job
↓
Pod completesThis is better than keeping a Worker continuously running for a job that only needs to execute once per day.
35. Azure Logic Apps
For workflow/integration scenarios:
Trigger
↓
Logic App
↓
Service A
↓
Service B
↓
EmailUseful for integration workflows rather than complex application business logic.
36. Azure Data Factory
For data movement/ETL:
SQL Server
↓
Azure Data Factory
↓
Transform
↓
Azure SQLIf your requirement is:
ETL
Data migration
Data integration
Scheduled data processingADF may be better than a Worker.
37. Azure WebJobs
For applications already hosted in Azure App Service, WebJobs can be useful for background execution.
Architecture:
Azure App Service
│
├── Web API
│
└── WebJob38. Which One Should You Choose?
A simple decision matrix:
| Requirement | Recommended |
|---|---|
| Long-running background process | Worker Service |
| Queue consumer | Worker Service / Azure Functions |
| Complex scheduled jobs | Quartz.NET |
| Simple background jobs | Hangfire |
| Serverless event processing | Azure Functions |
| Kubernetes scheduled batch | CronJob |
| ETL/Data movement | Azure Data Factory |
| Azure App Service background task | WebJobs |
| Complex integration workflow | Logic Apps |
39. Worker Service vs Azure Function
This is a common interview question.
Worker Service
You manage:
Application
Container/VM
Deployment
ScalingAzure Function
Azure manages:
Infrastructure
Scaling
RuntimeWorker:
Queue
↓
Worker
↓
ProcessFunction:
Queue
↓
Azure Function Trigger
↓
Process40. Worker Service vs Hangfire
Worker Service
Better for:
Continuous processing
Queue consumers
Long-running workloads
Microservice background processesHangfire
Better for:
Scheduled jobs
Fire-and-forget jobs
Recurring jobs
Retry management
Job dashboard41. Worker Service vs Kubernetes CronJob
Use Worker:
Continuous
↓
Consume messages
↓
Process continuouslyUse CronJob:
2 AM
↓
Start
↓
Process
↓
ExitFor example:
Order queue consumer:
Worker ServiceDaily database cleanup:
Kubernetes CronJob42. Recommended Microservice Architecture
For a production e-commerce system, I'd typically consider:
┌──────────────┐
│ Angular UI │
└──────┬───────┘
│
▼
┌───────────────┐
│ API Management│
└───────┬───────┘
│
┌───────────────┼────────────────┐
▼ ▼ ▼
Order API Payment API Customer API
│
▼
Azure Service Bus
│
┌──────┼─────────────┐
▼ ▼ ▼
Order Inventory Notification
Worker Worker Worker
│ │ │
▼ ▼ ▼
Azure Azure Email/
SQL SQL SMSAnd deploy using:
.NET Worker
↓
Docker
↓
Azure Container Registry
↓
AKS
↓
Pods
↓
Horizontal Scaling43. Production Best Practices
When implementing Worker Services, remember these:
1. Use CancellationToken
await ProcessAsync(cancellationToken);2. Use structured logging
_logger.LogInformation(
"Processing Order {OrderId}",
orderId);3. Don't inject scoped dependencies directly
Use:
IServiceScopeFactory4. Make processing idempotent
Prevent duplicate processing.
5. Implement retry
Transient failures should be retried.
6. Use Dead Letter Queue
Don't retry permanently bad messages forever.
7. Configure health monitoring
Monitor:
Worker status
Message count
Processing latency
Failure count
Retry count
DLQ count8. Use distributed tracing
For microservices:
API
↓
Service Bus
↓
Worker
↓
DatabaseYou should be able to trace the complete transaction/correlation flow.
44. Most Important Concept
The biggest idea to remember is:
Worker Services are not simply "another type of API." They are long-running background processes designed to execute work independently of incoming HTTP requests.
In microservices, they are particularly valuable for:
EVENT
│
▼
Message Broker
│
▼
Worker Service
│
┌──────┼──────┐
▼ ▼ ▼
DB API EventsThis allows your APIs to remain:
Fast
Stateless
Scalable
Responsivewhile the Worker handles:
Long-running
Asynchronous
Retryable
Queue-based
Scheduled
Backgroundprocessing.
Recommended architecture for your .NET/Azure stack
Given a typical .NET + Microservices + Azure + Docker + AKS architecture, a strong production combination is:
CLIENT
│
▼
API Management
│
▼
.NET Web API
│
Publish Event
│
▼
Azure Service Bus
│
┌───────────┼───────────┐
▼ ▼ ▼
.NET Worker .NET Worker .NET Worker
Order Inventory Notification
│ │ │
▼ ▼ ▼
Azure SQL Azure SQL External APIwith:
Docker
↓
Azure Container Registry
↓
AKS
↓
Worker Pods
↓
Horizontal ScalingThat gives you a clean event-driven microservice architecture with independent scaling and asynchronous processing.

No comments:
Post a Comment