Cache refreshment approaches

 

If you cache a SQL Server table in .NET Core, you need a way to refresh the cache when data changes. There are several approaches to achieve this:


1. Using SQL Server Query Notifications (SqlDependency)

Best for: Applications that need real-time cache updates with minimal overhead.

How It Works

  • SQL Server notifies your application when data changes, so you can refresh the cache.
  • Uses SqlDependency to listen for changes.
  • Requires Service Broker to be enabled in SQL Server.

Implementation Steps

Step 1: Enable Service Broker in SQL Server

Run this SQL command:

ALTER DATABASE YourDatabase SET ENABLE_BROKER;

Step 2: Configure SQL Dependency

Install the necessary package:

dotnet add package Microsoft.Data.SqlClient

Step 3: Implement Caching with Notifications

using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Caching.Memory;

public class ProductCacheService
{
    private readonly IMemoryCache _cache;
    private readonly string _connectionString;
    
    public ProductCacheService(IMemoryCache cache, IConfiguration config)
    {
        _cache = cache;
        _connectionString = config.GetConnectionString("DefaultConnection");

        StartSqlDependency();
    }

    public IEnumerable<Product> GetProducts()
    {
        if (!_cache.TryGetValue("ProductsCache", out IEnumerable<Product> products))
        {
            products = LoadProductsFromDatabase();
            _cache.Set("ProductsCache", products, TimeSpan.FromMinutes(10));
        }
        return products;
    }

    private void StartSqlDependency()
    {
        SqlDependency.Start(_connectionString);
        RefreshCache();
    }

    private void RefreshCache()
    {
        using var connection = new SqlConnection(_connectionString);
        using var command = new SqlCommand("SELECT Id, Name, Price FROM dbo.Products", connection);

        var dependency = new SqlDependency(command);
        dependency.OnChange += (sender, e) =>  
        {
            _cache.Remove("ProductsCache");
            GetProducts(); // Reload cache
        };

        connection.Open();
        command.ExecuteReader(); // Start listening
    }

    private IEnumerable<Product> LoadProductsFromDatabase()
    {
        using var connection = new SqlConnection(_connectionString);
        using var command = new SqlCommand("SELECT Id, Name, Price FROM dbo.Products", connection);
        connection.Open();

        using var reader = command.ExecuteReader();
        var products = new List<Product>();

        while (reader.Read())
        {
            products.Add(new Product
            {
                Id = reader.GetInt32(0),
                Name = reader.GetString(1),
                Price = reader.GetDecimal(2)
            });
        }

        return products;
    }
}

Pros:
✅ Automatic cache invalidation.
✅ No need to poll the database.

Cons:
❌ Requires Service Broker.
❌ Limited to queries that SQL Server can monitor.


2. Using Change Tracking (Polling)

Best for: When SqlDependency is not feasible (e.g., cloud databases like Azure SQL).

How It Works

  • Add a LastModified column to the table.
  • Use a background job (e.g., IHostedService) to check for changes periodically.
  • Refresh the cache if data has changed.

Implementation Steps

Step 1: Add LastModified Column in SQL Server

ALTER TABLE Products ADD LastModified DATETIME DEFAULT GETDATE();

Step 2: Implement Polling in .NET Core

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Hosting;
using System.Data.SqlClient;

public class CacheRefresherService : BackgroundService
{
    private readonly IMemoryCache _cache;
    private readonly string _connectionString;
    private DateTime _lastUpdated = DateTime.MinValue;

    public CacheRefresherService(IMemoryCache cache, IConfiguration config)
    {
        _cache = cache;
        _connectionString = config.GetConnectionString("DefaultConnection");
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await RefreshCacheIfNeeded();
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }

    private async Task RefreshCacheIfNeeded()
    {
        using var connection = new SqlConnection(_connectionString);
        using var command = new SqlCommand("SELECT MAX(LastModified) FROM Products", connection);
        connection.Open();
        
        var lastModified = (DateTime?)await command.ExecuteScalarAsync();
        if (lastModified > _lastUpdated)
        {
            _lastUpdated = lastModified ?? DateTime.MinValue;
            _cache.Remove("ProductsCache");
        }
    }
}

Step 3: Register Background Service in Program.cs

builder.Services.AddHostedService<CacheRefresherService>();

Pros:
✅ Works on any SQL Server version, including Azure SQL.
✅ No need for Service Broker.

Cons:
❌ Requires polling, which may increase database load.
❌ Not real-time (depends on the polling interval).


3. Using a Message Queue (Event-Driven)

Best for: High-performance distributed applications using multiple services.

How It Works

  • Use SQL Server triggers to publish changes to a message queue (e.g., RabbitMQ, Azure Service Bus).
  • A background worker listens for messages and refreshes the cache.

Implementation Steps

Step 1: Create a Trigger in SQL Server

CREATE TRIGGER ProductChangeTrigger
ON Products
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
    INSERT INTO ChangeLog (Entity, ChangeTime) VALUES ('Products', GETDATE());
END

Step 2: Implement a Message Listener in .NET Core

public class CacheUpdateListener : BackgroundService
{
    private readonly IMemoryCache _cache;

    public CacheUpdateListener(IMemoryCache cache)
    {
        _cache = cache;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (CheckForUpdates())
            {
                _cache.Remove("ProductsCache");
            }
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }

    private bool CheckForUpdates()
    {
        using var connection = new SqlConnection("YourConnectionString");
        using var command = new SqlCommand("SELECT TOP 1 ChangeTime FROM ChangeLog ORDER BY ChangeTime DESC", connection);
        connection.Open();

        var lastChange = (DateTime?)command.ExecuteScalar();
        return lastChange.HasValue && lastChange > DateTime.UtcNow.AddMinutes(-1);
    }
}

Step 3: Register the Background Service

builder.Services.AddHostedService<CacheUpdateListener>();

Pros:
Scalable – Works well in distributed environments.
Efficient – Only refreshes when needed.

Cons:
❌ Requires message queue setup.
❌ More complex than polling.


Which Approach Should You Choose?


Conclusion

  • Use SqlDependency for real-time cache invalidation, but only if Service Broker is available.
  • Use polling with a LastModified column if your database does not support Service Broker.
  • Use a message queue (e.g., RabbitMQ, Azure Service Bus) for distributed applications.

Comments

Popular posts from this blog

Maxpooling vs minpooling vs average pooling

Generative AI - Prompting with purpose: The RACE framework for data analysis

Best Practices for Storing and Loading JSON Objects from a Large SQL Server Table Using .NET Core