How to Implement Caching in a .NET Worker Service

Caching is commonly used in ASP.NET Core applications to improve performance and reduce database load. However, caching can be equally valuable in .NET Worker Services, especially when background jobs repeatedly fetch the same data from databases, APIs, or external services.

In this article, we'll explore how to implement caching in a .NET Worker Service using:
  1. In-Memory Cache (IMemoryCache)
  2. Distributed Cache (IDistributedCache with Redis)

By the end, you'll understand when to use each approach and how to integrate caching into your background processing workflows.

What is a .NET Worker Service?

A .NET Worker Service is a long-running background process built on the Generic Host infrastructure. It is commonly used for:
  • Scheduled jobs
  • Message queue processing
  • Data synchronization
  • Background monitoring
  • Batch processing

Since worker services often perform repetitive operations, caching can help reduce unnecessary resource consumption and improve throughput.

Why Use Caching in Worker Services?

Consider a worker service that processes thousands of records and frequently retrieves configuration data from a database.

Without caching:
  • Every processing cycle queries the database.
  • Database load increases.
  • Response times become slower.
  • Network overhead grows.

With caching:
  • Frequently used data is stored in memory.
  • Database calls are reduced.
  • Processing becomes faster.
  • Overall system scalability improves.

Scenario Suppose our Worker Service processes product data every 30 seconds. We'll create a service that:
  • Checks the cache.
  • If data exists, returns cached data.
  • Otherwise loads data from the database.
  • Stores the result in cache.

Demonstration: Caching Product Details from Database

To understand how caching improves performance in a Worker Service, let's build a simple example where the worker repeatedly retrieves product details from a database.

Scenario

Assume we have an e-commerce application where a Worker Service processes incoming orders. Each order contains a Product ID, and the worker needs to fetch product details before processing.

Without caching, the worker queries the database every time an order is processed.

With caching, product details are retrieved from the cache whenever available, significantly reducing database calls.

Product Entity

public class Product
{
  public int Id { get; set; }
  public string Name { get; set; } = string.Empty;
  public decimal Price { get; set; }
  public int StockQuantity { get; set; }
}
Repository for Database Access

public interface IProductRepository
{
  Task<Product?> GetProductAsync(int productId);
}
public class ProductRepository : IProductRepository
{
  public async Task<Product?> GetProductAsync(int productId)
  {
    Console.WriteLine( $"Fetching Product {productId} from Database");
    return new Product
    {
      Id = productId,
      Name = $"Product {productId}",
      Price = 999.99m,
      StockQuantity = 50
      };
    }
  }

Lets say, fetching product details takes two to three seconds

Demonstrating In-Memory Cache

Here’s a simple and beginner-friendly example of In-Memory Caching in C# using .Net in Worker Service. .Net provides a built-in in-memory cache through the IMemoryCache interface.

Install Required Package:- In most ASP.NET Core projects, memory caching is already included.If needed, install:

dotnet add package Microsoft.Extensions.Caching.Memory

Registration in Program.cs

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();

Cache Implementation

public class Worker : BackgroundService
{
  private readonly ProductService _productService;
  public Worker(ProductService productService)
  {
    _productService = productService;
  }
  protected override async Task ExecuteAsync(
  CancellationToken stoppingToken)
  {
    while (!stoppingToken.IsCancellationRequested)
    {
      var products =
      await _productService.GetProductsAsync();
      Console.WriteLine(
      $"Products Count: {products.Count}");
      await Task.Delay(
      TimeSpan.FromSeconds(30),
      stoppingToken);
    }
  }
}

Demonstrating Distributed Cache with Redis

When running multiple Worker Service instances, each instance has its own memory cache. This can lead to inconsistent cache data.

A distributed cache solves this problem by storing cache entries in a centralized location such as Redis.

Install Package

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Configure Redis Cache

builder.Services.AddStackExchangeRedisCache(options =>
{
  options.Configuration = "localhost:6379";
  options.InstanceName = "ProductCache:";
  });
builder.Services.AddSingleton<IProductRepository, ProductRepository>(); builder.Services.AddHostedService<Worker>();
Worker Using IDistributedCache

public class Worker : BackgroundService
{
  private readonly IDistributedCache _cache;
  private readonly IProductRepository _repository;
  private readonly ILogger<Worker> _logger;
  public Worker( IDistributedCache cache, IProductRepository repository, ILogger<Worker> logger) {
    _cache = cache;
    _repository = repository;
    _logger = logger;
  }
  protected override async Task ExecuteAsync( CancellationToken stoppingToken)
  {
    while (!stoppingToken.IsCancellationRequested)
    {
      var product = await GetProductAsync(1001);
      _logger.LogInformation( "Product: {Name}", product?.Name);
      await Task.Delay(5000, stoppingToken);
    }
  }
  private async Task<Product?> GetProductAsync( int productId)
  {
    string cacheKey = $"product:{productId}";
    var cachedProduct = await _cache.GetStringAsync(cacheKey);
    if (!string.IsNullOrEmpty(cachedProduct))
    {
      Console.WriteLine( $"Product {productId} loaded from Redis Cache");
      return JsonSerializer.Deserialize<Product>( cachedProduct);
    }
    var product = await _repository.GetProductAsync(productId);
    var options = new DistributedCacheEntryOptions
    {
      AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
      };
      await _cache.SetStringAsync( cacheKey, JsonSerializer.Serialize(product), options); Console.WriteLine( $"Product {productId} stored in Redis Cache");
      return product;
    }
  }

By implementing both approaches, you can demonstrate how a Worker Service first attempts to retrieve product data from the cache, falls back to the database when necessary, and then stores the result for future requests, dramatically reducing database load and improving processing performance.

Summary

Caching is an effective optimization strategy for .NET Worker Services. By using IMemoryCache, you can reduce database and API calls, improve processing speed, and lower infrastructure costs.

For single-instance worker services, in-memory caching provides a simple and efficient solution. For distributed deployments, consider Redis or another distributed cache provider to ensure consistency across multiple worker instances.

Implementing caching thoughtfully can significantly improve the performance, scalability, and reliability of your background processing workloads.

Thanks

Kailash Chandra Behera

I am an IT professional with over 13 years of experience in the full software development life cycle for Windows, services, and web-based applications using Microsoft .NET technologies.

Previous Post Next Post

نموذج الاتصال