ASP.NET Core: Implementing Caching with SQL Server

ASP.NET Core: Implementing Caching with SQL Server

·

5 min read

Caching is a powerful technique for improving the performance and scalability of your ASP.NET Core applications. By temporarily storing frequently accessed data in memory, caching reduces the need to query the database repeatedly. In this blog post, we’ll explore how to implement caching in an ASP.NET Core application using SQL Server as the data source, with examples using Entity Framework Core (EF Core).

What is Caching?

Caching is the process of storing copies of data in a temporary storage location, called a cache, so that future requests for that data can be served more quickly. In the context of a web application, caching can reduce database load and improve response times, leading to better user experience and reduced server costs.

Why Use Caching in ASP.NET Core?

Caching is particularly useful in scenarios where data does not change frequently and the cost of fetching it from the database is high. By caching the results of expensive queries or frequently accessed data, you can significantly reduce the load on your SQL Server and improve the overall performance of your application.

Step 1: Setting Up the ASP.NET Core Project

First, Create a new ASP.NET Core Web API project.

Next, add the necessary packages for Entity Framework Core and SQL Server.

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Extensions.Caching.Memory
dotnet add package Microsoft.Extensions.Caching.SqlServer

Step 2: Configuring EF Core to Use SQL Server

We’ll start by creating a simple model and a corresponding DbContext to interact with SQL Server.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("YourConnectionStringHere");
    }
}

Update Startup.cs to register the AppDbContext with the dependency injection container.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddMemoryCache(); // Register in-memory caching
    services.AddControllers();
}

Step 3: Implementing In-Memory Caching

In-memory caching stores data in the memory of the web server. It’s a fast and simple way to cache data but is only effective for single-instance applications.

Here’s how to implement in-memory caching for a GetProducts method in a controller.

[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly IMemoryCache _cache;
    private const string CacheKey = "products_cache";

    public ProductsController(AppDbContext context, IMemoryCache cache)
    {
        _context = context;
        _cache = cache;
    }

    [HttpGet]
    public async Task<IEnumerable<Product>> GetProducts()
    {
        if (!_cache.TryGetValue(CacheKey, out IEnumerable<Product> products))
        {
            products = await _context.Products.ToListAsync();

            var cacheOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
                SlidingExpiration = TimeSpan.FromMinutes(2)
            };

            _cache.Set(CacheKey, products, cacheOptions);
        }

        return products;
    }
}

Step 4: Using Distributed Caching with SQL Server

In-memory caching is limited to the memory of the server on which the application is running. For a distributed application or multiple server instances, you’ll need to use distributed caching. ASP.NET Core supports distributed caching with SQL Server.

First, configure distributed caching in Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = Configuration.GetConnectionString("Connection of the database that will holde cache");
        options.SchemaName = "dbo";
        options.TableName = "CacheTableName";
    });

    services.AddControllers();
}

NOTE:

Create you cache table by executing the following SQL script:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[CacheTableName](
    [Id] [nvarchar](449) NOT NULL,
    [Value] [varbinary](max) NOT NULL,
    [ExpiresAtTime] [datetimeoffset](7) NOT NULL,
    [SlidingExpirationInSeconds] [bigint] NULL,
    [AbsoluteExpiration] [datetimeoffset](7) NULL,
PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
    IGNORE_DUP_KEY = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON, 
    OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

Then, update the ProductsController to use distributed caching.

public class ProductsController : ControllerBase
{
    private readonly AppDbContext _context;
    private readonly IDistributedCache _cache;
    private const string CacheKey = "products_cache";

    public ProductsController(AppDbContext context, IDistributedCache cache)
    {
        _context = context;
        _cache = cache;
    }

    [HttpGet]
    public async Task<IEnumerable<Product>> GetProducts()
    {
        var cachedProducts = await _cache.GetStringAsync(CacheKey);
        if (cachedProducts != null)
        {
            return JsonSerializer.Deserialize<IEnumerable<Product>>(cachedProducts);
        }

        var products = await _context.Products.ToListAsync();
        var serializedProducts = JsonSerializer.Serialize(products);

        await _cache.SetStringAsync(CacheKey, serializedProducts, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
            SlidingExpiration = TimeSpan.FromMinutes(2)
        });

        return products;
    }
}

Best Practices for Implementing Caching in ASP.NET Core

Caching is a powerful tool, but it should be used thoughtfully to avoid potential pitfalls. Here are some best practices to consider when implementing caching in your ASP.NET Core application:

1. Cache Appropriately

Not all data should be cached. Cache data that is expensive to retrieve and does not change frequently. For example, consider caching reference data, configuration settings, or results of complex calculations.

2. Use Expiration Policies

Set appropriate expiration policies to prevent stale data from being served. Absolute expiration sets a fixed duration after which the cache entry is invalidated, while sliding expiration resets the expiration time whenever the cached data is accessed.

3. Monitor Cache Usage

Keep an eye on cache usage and performance metrics. Monitor cache hit rates, memory usage, and database load to ensure that caching is providing the expected benefits.

4. Handle Cache Invalidation

Implement a strategy for cache invalidation when underlying data changes. This could involve clearing the cache when data is updated or using cache tags to invalidate related cache entries.

5. Consider Cache Size

For in-memory caching, consider the size of the data being cached and the available memory on your server. Large cache sizes can lead to memory pressure and potentially degrade application performance.

6. Leverage Distributed Caching for Scalability

If your application runs on multiple servers or instances, use distributed caching to ensure consistency across servers. SQL Server distributed caching is a reliable option, but you could also explore other distributed caching solutions like Redis or NCache.