Implementing a Worker Service for Newsletter Service in .NET


Sending newsletters is a classic background task. You don’t want email delivery to block HTTP requests, slow down users, or fail silently when traffic spikes. This is where a .NET Worker Service fits perfectly.

In this article, we’ll build a Newsletter Worker Service using .NET that runs independently, processes newsletter jobs in the background, and can be hosted as a Windows Service, Linux daemon, or Docker container.

Why Use a Worker Service for Newsletters?

Sending newsletters inside a controller or API call is risky:

  • Email sending is slow and I/O heavy
  • Failures are hard to retry
  • App restarts can interrupt delivery
  • Scaling becomes painful

A Worker Service solves this by:

  • Running independently of web requests
  • Processing jobs asynchronously
  • Supporting retries, logging, and graceful shutdown
  • Scaling horizontally

What Is a .NET Worker Service?

A Worker Service is a long-running background process built on IHostedService.

Typical use cases:

  • Email & notification sending
  • Queue consumers
  • Scheduled jobs
  • Data sync
  • Report generation

It runs using the Generic Host, same infrastructure used by ASP.NET Core.

Architecture Overview

Simple Newsletter Flow

  • Web App stores newsletter job in DB / Queue
  • Worker Service polls or listens
  • Worker sends emails
  • Status updated (Sent / Failed / Retried)
Web App → Database / Queue → Worker Service → SMTP / Email API

Step 1: Create a Worker Service Project

dotnet new worker -n Newsletter.Worker
cd Newsletter.Worker

Project structure:

Newsletter.Worker
 ├── Program.cs
 ├── Worker.cs
 ├── appsettings.json

Step 2: Configure the Host

Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
        services.AddSingleton<INewsletterService, NewsletterService>();
    })
    .Build();

await host.RunAsync();

Step 3: Implement the Worker

Worker.cs

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly INewsletterService _newsletterService;

    public Worker(
        ILogger<Worker> logger,
        INewsletterService newsletterService)
    {
        _logger = logger;
        _newsletterService = newsletterService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Newsletter Worker started.");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await _newsletterService.ProcessPendingNewslettersAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error while processing newsletters");
            }

            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }

        _logger.LogInformation("Newsletter Worker stopping.");
    }
}

Step 4: Newsletter Service Logic

INewsletterService.cs

public interface INewsletterService
{
    Task ProcessPendingNewslettersAsync(CancellationToken token);
}

NewsletterService.cs

public class NewsletterService : INewsletterService
{
    public async Task ProcessPendingNewslettersAsync(CancellationToken token)
    {
        var newsletters = GetPendingNewsletters();

        foreach (var newsletter in newsletters)
        {
            if (token.IsCancellationRequested)
                break;

            try
            {
                await SendEmailAsync(newsletter);
                MarkAsSent(newsletter.Id);
            }
            catch
            {
                MarkAsFailed(newsletter.Id);
            }
        }
    }

    private List<Newsletter> GetPendingNewsletters()
    {
        // Fetch from DB or queue
        return new List<Newsletter>();
    }

    private Task SendEmailAsync(Newsletter newsletter)
    {
        // SMTP / SendGrid / SES logic
        return Task.CompletedTask;
    }

    private void MarkAsSent(int id) { }
    private void MarkAsFailed(int id) { }
}

Step 5: Email Sending Strategy

You can plug in:

  • SMTP (System.Net.Mail)
  • SendGrid
  • Amazon SES
  • Azure Communication Services

Always:

  • Use async APIs
  • Set timeouts
  • Log failures
  • Avoid blocking calls

Step 6: Configuration via appsettings.json

{
  "Worker": {
    "IntervalSeconds": 30
  },
  "Smtp": {
    "Host": "smtp.example.com",
    "Port": 587,
    "Username": "user",
    "Password": "password"
  }
}

Bind configs using IOptions<T> for clean code.

Step 7: Logging & Observability

Use structured logging:

_logger.LogInformation(
    "Sending newsletter {NewsletterId} to {Email}",
    newsletter.Id,
    newsletter.Email);

Recommended:

  • Serilog
  • Seq / ELK
  • Application Insights

Step 8: Graceful Shutdown

Worker services automatically handle shutdown signals:

  • CTRL + C
  • Windows Service stop
  • Docker stop

Using CancellationToken ensures:

  • No half-sent emails
  • Clean exit
  • No data corruption

Step 9: Deployment Options

You can host the worker as:

  • Windows Service
  • Linux systemd service
  • Docker container
  • Kubernetes Job / Deployment
  • Azure App Service (Worker)

Common Enhancements

  • Retry policy (Polly)
  • Queue-based processing (RabbitMQ, Azure Service Bus)
  • Batch sending
  • Rate limiting
  • Dead-letter handling
  • Health checks

When NOT to Use a Worker Service

  • Very small apps with few emails
  • Real-time user notifications
  • One-off scripts
  • In those cases, simpler background jobs may suffice.

Final Thoughts

A .NET Worker Service is a clean, scalable, and production-ready way to handle newsletter delivery. It keeps your web app fast, improves reliability, and gives you full control over retries, failures, and scaling.

0 Comments Report