Implementing Caching in ASP.NET

When it comes to performance and scalability optimization, cache is the most crucial factor. By using caching in NET Core, you may improve user happiness, speed up answers, and lessen the strain on the database. I'll be showing you how to use caching in this blog. This blog post will guide you through configuring caching in NET Core, covering various methods and suggested strategies.

Introduction to Caching in ASP.NET

In order to minimize the time and resources needed to access data, caching is putting the data in a temporary storage location. Caching can be useful in the context of online applications.

  • Improve Performance

    Applications can swiftly fulfill requests without continuously searching the database or recreating content by caching frequently requested data.

  • Reduce Server Load

    By lowering the quantity of requests that arrive at the server, caching helps the server handle more concurrent users and lessens load.

  • Enhance User Experience

    Quicker reaction times improve the user experience and maintain users' interest and satisfaction.

It helps to understand dependency injection and service patterns in order to comprehend caching in ASP.NET. See my earlier articles on Dependency Injection and Service Patterns if you're unfamiliar with these ideas.

Different Caching Techniques in ASP.NET

Output Caching

With output caching, a web page's complete HTTP response is saved and can be retrieved again for a subsequent request. This works especially well for pages that are static or change seldom.

How it works:

  1. Declaration: Use the [OutputCache] attribute on your controller actions.

  2. Configuration: Specify parameters like duration, location, and cache profiles.

Use Cases:

  • Static pages

  • Data that changes infrequently

Code Implementation:

[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
    return View();
}

Fragment Caching

You can cache specific parts of a webpage, like user controls or partial views, by using fragment caching. When a page is just partially dynamic, this is helpful.

How it works:

  1. Declaration: Use the [OutputCache] attribute on child actions or partial views.

  2. Configuration: Similar to output caching, specify duration and other parameters.

Use Cases:

  • Frequently used widgets

  • Partially dynamic pages

Code Implementation:

[ChildActionOnly]
[OutputCache(Duration = 60)]
public ActionResult Menu()
{
    return PartialView("_Menu");
}

Data Caching

Data caching is the process of storing data that has been retrieved from a database or another data source. This can significantly reduce the number of database requests, which can improve application speed.

How it works:

  1. Cache Insert: Store data in the cache.

  2. Cache Retrieve: If data is available, retrieve it from the cache; if not, obtain it from the database and save it.

Use Cases:

  • Frequently accessed data

  • Expensive database queries

Code Implementation:

var cacheKey = "productList";
var products = HttpRuntime.Cache[cacheKey] as List<Product>;

if (products == null)
{
    products = GetProductsFromDatabase();
    HttpRuntime.Cache.Insert(cacheKey, products, null, DateTime.Now.AddMinutes(10), Cache.NoSlidingExpiration);
}

return products;

Memory Caching

Memory caching allows for quick access to data by storing it in the application's memory. For this, ASP.NET Core makes use of IMemoryCache.

How it works:

  1. Registration: Add memory caching services in Startup.cs.

  2. Usage: Use IMemoryCache to store and retrieve data.

Use Cases:

  • Frequently accessed data

  • Non-distributed caching

Code Implementation:

public class ProductService
{
    private readonly IMemoryCache _cache;

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

    public List<Product> GetProducts()
    {
        var cacheKey = "productList";
        if (!_cache.TryGetValue(cacheKey, out List<Product> products))
        {
            products = GetProductsFromDatabase();
            _cache.Set(cacheKey, products, TimeSpan.FromMinutes(10));
        }
        return products;
    }
}

Caching Implementation Examples

The basic CRUD application will serve as the foundation for the very basic application that we will construct in this tutorial. Check out the ASPNANO project to see how caching is applied in a more general setting. With thorough documentation on ideas like clean design, programming patterns, and practices, ASPNANO is an excellent learning tool. Compared to other options like the ABP framework, this beginning template is far easier.

The main reasons to use caching:

  • Decreases database load to enhance application performance

  • Enhances the scalability of your application

  • Makes the end user's experience more seamless

To follow along, get the example app code from GitHub here. The CRUD app lesson uses the same code, however this post concentrates on

Application Structure

First, let's talk about the structure of our application. .NET will add these lines to Program.cs and establish a Controllers folder if this project is created as a "Web API" project. This will appear in both .NET 6 and .NET 7 versions.

var builder = WebApplication.CreateBuilder(args); // <--- 1. Create the Builder (ASP.NET convention)

// SERVICES
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build(); // <--- 2. Build the App (ASP.NET convention)

// MIDDLEWARE
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run(); // <--- 3. Run the App (ASP.NET convention)

There would be a Pages folder with sample razor pages if you had made a "Web App" project (razor pages). Although support for razor pages would take the role of API controllers, it is still possible to add API support and utilize both razor pages and API controllers in the same project.

We will need to add more services to the DI container in order to cache data. You may save a ton of time by using the powerful caching configuration that comes preinstalled with the ASPNANO boilerplate.

Setting Up Caching

We must add cache services to the DI container in Program.cs in order to configure caching in our ASP.NET application.

// Adding caching services
builder.Services.AddMemoryCache();
builder.Services.AddDistributedMemoryCache();

While MemoryCache works well in basic circumstances, distributed caching is a better option for applications that require more scalability and resilience. The documentation offered by ASPNANO gives a great summary of the trade-offs associated with various caching techniques.

Setting Up Memory Caching

"In-memory caching" is the most fundamental kind of caching in ASP.NET. The web server's memory holds the information.

Here's how to configure it:

public class ProductsController : ControllerBase
{
    private readonly IMemoryCache _cache;
    private readonly IProductService _productService;

    public ProductsController(IMemoryCache cache, IProductService productService)
    {
        _cache = cache;
        _productService = productService;
    }

    [HttpGet]
    public IActionResult Get()
    {
        var cacheKey = "productList";
        if (!_cache.TryGetValue(cacheKey, out List<Product> productList))
        {
            productList = _productService.GetAllProducts().ToList();
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(60));

            _cache.Set(cacheKey, productList, cacheEntryOptions);
        }

        return Ok(productList);
    }
}

This example demonstrates how to save and retrieve a product list using in-memory caching. Explore ASPNANO, which has built-in support for a number of caching algorithms, for more sophisticated caching techniques.

Using Distributed Caching

For applications that need to expand out across numerous servers, distributed caching is more appropriate. To set up Redis for distributed caching, follow these steps:

Install the Redis package first:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Then, configure Redis in Program.cs:

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "SampleInstance";
});

And use it in your controller:

public class ProductsController : ControllerBase
{
    private readonly IDistributedCache _cache;
    private readonly IProductService _productService;

    public ProductsController(IDistributedCache cache, IProductService productService)
    {
        _cache = cache;
        _productService = productService;
    }

    public async Task<IActionResult> Get()
    {
        var cacheKey = "productList";
        string serializedProductList;
        var productList = new List<Product>();

        var cachedProductList = await _cache.GetStringAsync(cacheKey);

        if (!string.IsNullOrEmpty(cachedProductList))
        {
            serializedProductList = cachedProductList;
            productList = JsonConvert.DeserializeObject<List<Product>>(serializedProductList);
        }
        else
        {
            productList = _productService.GetAllProducts().ToList();
            serializedProductList = JsonConvert.SerializeObject(productList);
            var options = new DistributedCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(30));

            await _cache.SetStringAsync(cacheKey, serializedProductList, options);
        }

        return Ok(productList);
    }
}

In this case, caching the product list with Redis offers a more scalable solution. Two services that work well with ASP.NET that can be used for more complex distributed caching methods are AWS ElastiCache and Azure Cache for Redis. ASPNANO offers detailed instructions for integrating these services into your apps.

Implementing a Custom Cache Provider

It may be necessary to develop a custom cache provider for certain requirements. For creating custom providers and incorporating them into your applications, ASPNANO offers a wealth of documentation.

This is an illustration of how to construct a custom cache provider:

public interface ICustomCacheProvider
{
    Task<T> GetAsync<T>(string cacheKey);
    Task SetAsync<T>(string cacheKey, T value, TimeSpan expiration);
}

public class CustomCacheProvider : ICustomCacheProvider
{
    private readonly IDistributedCache _cache;

    public CustomCacheProvider(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task<T> GetAsync<T>(string cacheKey)
    {
        var cachedValue = await _cache.GetStringAsync(cacheKey);
        if (cachedValue == null)
        {
            return default;
        }
        return JsonConvert.DeserializeObject<T>(cachedValue);
    }

    public async Task SetAsync<T>(string cacheKey, T value, TimeSpan expiration)
    {
        var serializedValue = JsonConvert.SerializeObject(value);
        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration
        };
        await _cache.SetStringAsync(cacheKey, serializedValue, options);
    }
}

You can then use this custom provider in your controllers:

public class ProductsController : ControllerBase
{
    private readonly ICustomCacheProvider _cache;
    private readonly IProductService _productService;

    public ProductsController(ICustomCacheProvider cache, IProductService productService)
    {
        _cache = cache;
        _productService = productService;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var cacheKey = "productList";
        var productList = await _cache.GetAsync<List<Product>>(cacheKey);

        if (productList == null)
        {
            productList = _productService.GetAllProducts().ToList();
            await _cache.SetAsync(cacheKey, productList, TimeSpan.FromMinutes(30));
        }

        return Ok(productList);
    }
}

You have freedom and control over your caching strategy with custom cache providers. See ASPNANO's guidance on custom providers and caching algorithms for more complex cases.

Best Practices for Caching

Following best practices is necessary for caching to be implemented effectively:

  1. Choose the Right Cache Strategy

    Make the right choice for your application's needs by understanding the differences between distributed and in-memory caching.

  2. Cache Invalidation

    To prevent stale data, make sure your caching strategy has appropriate invalidation mechanisms.

  3. Cache Expiration

    Apply the proper expiration rules (sliding or absolute) in accordance with usage trends and statistics.

  4. Monitor and Tune

    To improve and adjust your caching approach, keep an eye on cache utilization and performance on a regular basis.

Final Thoughts

Caching is a useful method for improving an application's performance and scalability. Using both distributed and in-memory caching in your ASP.NET applications can significantly enhance user experience.

I strongly advise looking into ASPNANO, which provides a plethora of knowledge and resources on caching, speed optimization, and other best practices in ASP.NET programming, for additional reading and advanced caching techniques.

Did you find this article valuable?

Support Rahul's Blog by becoming a sponsor. Any amount is appreciated!